Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _examples/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type CreateUserInput struct {
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=13,max=120"`
Bio string `json:"bio" validate:"omitempty,max=500"`
Internal string `json:"internal" openapi:"-"` // Hidden from OpenAPI docs, still parsed at runtime

RequestContext ContextRequest `json:"requestContext" validate:"required,dive"`
}
Expand All @@ -41,6 +42,7 @@ type CreateUserOutput struct {
Message string `json:"message"`
Username string `json:"username"`
Email string `json:"email"`
Token string `json:"token" openapi:"-"` // Hidden from OpenAPI docs, still serialized in response
}

type CreateUserError struct {
Expand Down
5 changes: 5 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ func extractParametersFromStruct(inputType reflect.Type) []map[string]interface{
continue
}

// Skip fields hidden from OpenAPI documentation
if field.Tag.Get("openapi") == "-" {
continue
}

// Process path parameters
if pathTag := field.Tag.Get("path"); pathTag != "" {
// Path parameters are always required regardless of type or validation tags.
Expand Down
10 changes: 10 additions & 0 deletions fiberoapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ func collectAllTypes(t reflect.Type, collected map[string]reflect.Type) {
continue
}

// Skip fields hidden from OpenAPI documentation
if field.Tag.Get("openapi") == "-" {
continue
}

// Skip fields with json:"-" tag
if jsonTag := field.Tag.Get("json"); jsonTag == "-" {
continue
Expand Down Expand Up @@ -575,6 +580,11 @@ func generateSchema(t reflect.Type) map[string]interface{} {
continue
}

// Skip fields hidden from OpenAPI documentation
if field.Tag.Get("openapi") == "-" {
continue
}

// Skip fields that are path, query, or header parameters - they are handled separately
if field.Tag.Get("path") != "" || field.Tag.Get("query") != "" || field.Tag.Get("header") != "" {
continue
Expand Down
135 changes: 135 additions & 0 deletions openapi_hidden_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package fiberoapi

import (
"encoding/json"
"io"
"net/http/httptest"
"testing"

"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Structs for testing openapi:"-" tag

type HiddenFieldInput struct {
Name string `json:"name" validate:"required"`
Internal string `json:"internal" openapi:"-"`
}

type HiddenFieldOutput struct {
ID int `json:"id"`
Name string `json:"name"`
Secret string `json:"secret" openapi:"-"`
}

type HiddenFieldError struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
}

type HiddenQueryInput struct {
Name string `query:"name"`
Hidden string `query:"hidden" openapi:"-"`
}

func TestOpenAPIHiddenField_ExcludedFromBodySchema(t *testing.T) {
app := fiber.New()
oapi := New(app)

Post(oapi, "/items", func(c *fiber.Ctx, input *HiddenFieldInput) (*HiddenFieldOutput, *HiddenFieldError) {
return &HiddenFieldOutput{ID: 1, Name: input.Name}, nil
}, OpenAPIOptions{
OperationID: "createItem",
Summary: "Create item",
Tags: []string{"items"},
})

oapi.SetupDocs()

req := httptest.NewRequest("GET", "/openapi.json", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

var spec map[string]interface{}
require.NoError(t, json.Unmarshal(body, &spec))

components, ok := spec["components"].(map[string]interface{})
require.True(t, ok, "expected components section in OpenAPI spec")
schemas, ok := components["schemas"].(map[string]interface{})
require.True(t, ok, "expected schemas section in components")

// Check input schema — "internal" field should be hidden
inputSchema, ok := schemas["HiddenFieldInput"].(map[string]interface{})
require.True(t, ok, "expected HiddenFieldInput schema")
inputProps, ok := inputSchema["properties"].(map[string]interface{})
require.True(t, ok, "expected properties in HiddenFieldInput schema")
_, hasName := inputProps["name"]
_, hasInternal := inputProps["internal"]
assert.True(t, hasName, "visible field 'name' should be in schema")
assert.False(t, hasInternal, "hidden field 'internal' should NOT be in schema")

// Check output schema — "secret" field should be hidden
outputSchema, ok := schemas["HiddenFieldOutput"].(map[string]interface{})
require.True(t, ok, "expected HiddenFieldOutput schema")
outputProps, ok := outputSchema["properties"].(map[string]interface{})
require.True(t, ok, "expected properties in HiddenFieldOutput schema")
_, hasID := outputProps["id"]
_, hasSecret := outputProps["secret"]
assert.True(t, hasID, "visible field 'id' should be in schema")
assert.False(t, hasSecret, "hidden field 'secret' should NOT be in schema")
}

func TestOpenAPIHiddenField_ExcludedFromQueryParams(t *testing.T) {
app := fiber.New()
oapi := New(app)

Get(oapi, "/search", func(c *fiber.Ctx, input HiddenQueryInput) (*HiddenFieldOutput, *HiddenFieldError) {
return &HiddenFieldOutput{ID: 1, Name: input.Name}, nil
}, OpenAPIOptions{
OperationID: "searchItems",
Summary: "Search items",
})

oapi.SetupDocs()

req := httptest.NewRequest("GET", "/openapi.json", nil)
resp, err := app.Test(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

var spec map[string]interface{}
require.NoError(t, json.Unmarshal(body, &spec))

paths, ok := spec["paths"].(map[string]interface{})
require.True(t, ok, "expected paths section in OpenAPI spec")
searchPath, ok := paths["/search"].(map[string]interface{})
require.True(t, ok, "expected /search path in spec")
getOp, ok := searchPath["get"].(map[string]interface{})
require.True(t, ok, "expected get operation on /search")

params, ok := getOp["parameters"].([]interface{})
require.True(t, ok, "expected parameters array on /search get operation")

// Verify "name" is present and "hidden" is absent
foundName := false
for _, p := range params {
param, ok := p.(map[string]interface{})
require.True(t, ok, "expected parameter to be a map")
if param["name"] == "name" {
foundName = true
}
assert.NotEqual(t, "hidden", param["name"], "hidden query param should NOT appear in OpenAPI parameters")
}
assert.True(t, foundName, "visible query param 'name' should appear in OpenAPI parameters")
}
Loading