diff --git a/_examples/simple/main.go b/_examples/simple/main.go index d849a11..f69621b 100644 --- a/_examples/simple/main.go +++ b/_examples/simple/main.go @@ -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"` } @@ -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 { diff --git a/common.go b/common.go index 79b3c16..baec2b2 100644 --- a/common.go +++ b/common.go @@ -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. diff --git a/fiberoapi.go b/fiberoapi.go index 3c31b11..1c1ee36 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -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 @@ -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 diff --git a/openapi_hidden_test.go b/openapi_hidden_test.go new file mode 100644 index 0000000..714f13d --- /dev/null +++ b/openapi_hidden_test.go @@ -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") +}