diff --git a/internal/json_schema.go b/internal/json_schema.go index 4bcda10..b90df9b 100644 --- a/internal/json_schema.go +++ b/internal/json_schema.go @@ -3,6 +3,7 @@ package internal import ( "bytes" "encoding/json" + "fmt" "mime/multipart" "net/http" "reflect" @@ -18,6 +19,9 @@ const ( tagJSON = "json" tagFormData = "formData" tagForm = "form" + tagHeader = "header" + + componentsSchemas = "#/components/schemas/" ) var defNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9.\-_]+`) @@ -73,8 +77,19 @@ func ReflectRequestBody( return nil, false, nil } + // Checking for default options that allow tag-less JSON. + isProcessWithoutTags := false + _, err = r.Reflect("", func(rc *jsonschema.ReflectContext) { + isProcessWithoutTags = rc.ProcessWithoutTags + }) + + if err != nil { + return nil, false, fmt.Errorf("BUG: %w", err) + } + // JSON can be a map or array without field tags. - if !hasTaggedFields && len(mapping) == 0 && !refl.IsSliceOrMap(input) && refl.FindEmbeddedSliceOrMap(input) == nil { + if !hasTaggedFields && len(mapping) == 0 && !refl.IsSliceOrMap(input) && + refl.FindEmbeddedSliceOrMap(input) == nil && !isProcessWithoutTags { return nil, false, nil } @@ -192,3 +207,65 @@ func hasJSONBody(r *jsonschema.Reflector, output interface{}) (bool, error) { return false, nil } + +// ReflectResponseHeader reflects response headers from content unit. +func ReflectResponseHeader( + r *jsonschema.Reflector, + oc openapi.OperationContext, + cu openapi.ContentUnit, + interceptProp jsonschema.InterceptPropFunc, +) (jsonschema.Schema, error) { + output := cu.Structure + mapping := cu.FieldMapping(openapi.InHeader) + + if output == nil { + return jsonschema.Schema{}, nil + } + + return r.Reflect(output, + func(rc *jsonschema.ReflectContext) { + rc.ProcessWithoutTags = false + }, + openapi.WithOperationCtx(oc, true, openapi.InHeader), + jsonschema.InlineRefs, + jsonschema.PropertyNameMapping(mapping), + jsonschema.PropertyNameTag(tagHeader), + sanitizeDefName, + jsonschema.InterceptProp(interceptProp), + ) +} + +// ReflectParametersIn reflects JSON schema of request parameters. +func ReflectParametersIn( + r *jsonschema.Reflector, + oc openapi.OperationContext, + c openapi.ContentUnit, + in openapi.In, + collectDefinitions func(name string, schema jsonschema.Schema), + interceptProp jsonschema.InterceptPropFunc, + additionalTags ...string, +) (jsonschema.Schema, error) { + input := c.Structure + propertyMapping := c.FieldMapping(in) + + if refl.IsSliceOrMap(input) { + return jsonschema.Schema{}, nil + } + + return r.Reflect(input, + func(rc *jsonschema.ReflectContext) { + rc.ProcessWithoutTags = false + }, + openapi.WithOperationCtx(oc, false, in), + jsonschema.DefinitionsPrefix(componentsSchemas), + jsonschema.CollectDefinitions(collectDefinitions), + jsonschema.PropertyNameMapping(propertyMapping), + jsonschema.PropertyNameTag(string(in), additionalTags...), + func(rc *jsonschema.ReflectContext) { + rc.UnnamedFieldWithTag = true + }, + sanitizeDefName, + jsonschema.SkipEmbeddedMapsSlices, + jsonschema.InterceptProp(interceptProp), + ) +} diff --git a/openapi3/reflect.go b/openapi3/reflect.go index a05a5d4..475ad12 100644 --- a/openapi3/reflect.go +++ b/openapi3/reflect.go @@ -380,27 +380,17 @@ func (r *Reflector) parseParametersIn( in openapi.In, additionalTags ...string, ) error { - input := c.Structure - propertyMapping := c.FieldMapping(in) - - if refl.IsSliceOrMap(input) { + if refl.IsSliceOrMap(c.Structure) { return nil } - definitionsPrefix := componentsSchemas - - s, err := r.Reflect(input, - openapi.WithOperationCtx(oc, false, in), - jsonschema.DefinitionsPrefix(definitionsPrefix), - jsonschema.CollectDefinitions(r.collectDefinition()), - jsonschema.PropertyNameMapping(propertyMapping), - jsonschema.PropertyNameTag(string(in), additionalTags...), - func(rc *jsonschema.ReflectContext) { - rc.UnnamedFieldWithTag = true - }, - sanitizeDefName, - jsonschema.SkipEmbeddedMapsSlices, - jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error { + s, err := internal.ReflectParametersIn( + r.JSONSchemaReflector(), + oc, + c, + in, + r.collectDefinition(), + func(params jsonschema.InterceptPropParams) error { if !params.Processed || len(params.Path) > 1 { return nil } @@ -442,7 +432,7 @@ func (r *Reflector) parseParametersIn( if refl.HasTaggedFields(property, tagJSON) && !refl.HasTaggedFields(property, string(in)) { propertySchema, err := r.Reflect(property, openapi.WithOperationCtx(oc, false, in), - jsonschema.DefinitionsPrefix(definitionsPrefix), + jsonschema.DefinitionsPrefix(componentsSchemas), jsonschema.CollectDefinitions(r.collectDefinition()), jsonschema.RootRef, sanitizeDefName, @@ -495,8 +485,7 @@ func (r *Reflector) parseParametersIn( o.Parameters = append(o.Parameters, ParameterOrRef{Parameter: &p}) return nil - }), - ) + }, additionalTags...) if err != nil { return err } @@ -532,22 +521,14 @@ func (r *Reflector) collectDefinition() func(name string, schema jsonschema.Sche } func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error { - output := cu.Structure - mapping := cu.FieldMapping(openapi.InHeader) - - if output == nil { + if cu.Structure == nil { return nil } res := make(map[string]HeaderOrRef) - schema, err := r.Reflect(output, - openapi.WithOperationCtx(oc, true, openapi.InHeader), - jsonschema.InlineRefs, - jsonschema.PropertyNameMapping(mapping), - jsonschema.PropertyNameTag(tagHeader), - sanitizeDefName, - jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error { + schema, err := internal.ReflectResponseHeader(r.JSONSchemaReflector(), oc, cu, + func(params jsonschema.InterceptPropParams) error { if !params.Processed || len(params.Path) > 1 { // only top-level fields (including embedded). return nil } @@ -579,7 +560,7 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont } return nil - }), + }, ) if err != nil { return err diff --git a/openapi31/reflect.go b/openapi31/reflect.go index 11a66b2..98fd46a 100644 --- a/openapi31/reflect.go +++ b/openapi31/reflect.go @@ -224,7 +224,6 @@ const ( tagJSON = "json" tagFormData = "formData" tagForm = "form" - tagHeader = "header" mimeJSON = "application/json" mimeFormUrlencoded = "application/x-www-form-urlencoded" mimeMultipart = "multipart/form-data" @@ -333,27 +332,12 @@ func (r *Reflector) parseParametersIn( in openapi.In, additionalTags ...string, ) error { - input := c.Structure - propertyMapping := c.FieldMapping(in) - - if refl.IsSliceOrMap(input) { + if refl.IsSliceOrMap(c.Structure) { return nil } - definitionsPrefix := componentsSchemas - - s, err := r.Reflect(input, - openapi.WithOperationCtx(oc, false, in), - jsonschema.DefinitionsPrefix(definitionsPrefix), - jsonschema.CollectDefinitions(r.collectDefinition()), - jsonschema.PropertyNameMapping(propertyMapping), - jsonschema.PropertyNameTag(string(in), additionalTags...), - func(rc *jsonschema.ReflectContext) { - rc.UnnamedFieldWithTag = true - }, - sanitizeDefName, - jsonschema.SkipEmbeddedMapsSlices, - jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error { + s, err := internal.ReflectParametersIn( + r.JSONSchemaReflector(), oc, c, in, r.collectDefinition(), func(params jsonschema.InterceptPropParams) error { if !params.Processed || len(params.Path) > 1 { return nil } @@ -393,7 +377,7 @@ func (r *Reflector) parseParametersIn( if refl.HasTaggedFields(property, tagJSON) && !refl.HasTaggedFields(property, string(in)) { //nolint:nestif propertySchema, err := r.Reflect(property, openapi.WithOperationCtx(oc, false, in), - jsonschema.DefinitionsPrefix(definitionsPrefix), + jsonschema.DefinitionsPrefix(componentsSchemas), jsonschema.CollectDefinitions(r.collectDefinition()), jsonschema.RootRef, sanitizeDefName, @@ -449,7 +433,7 @@ func (r *Reflector) parseParametersIn( o.Parameters = append(o.Parameters, ParameterOrReference{Parameter: &p}) return nil - }), + }, additionalTags..., ) if err != nil { return err @@ -488,22 +472,14 @@ func (r *Reflector) collectDefinition() func(name string, schema jsonschema.Sche } func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error { - output := cu.Structure - mapping := cu.FieldMapping(openapi.InHeader) - - if output == nil { + if cu.Structure == nil { return nil } res := make(map[string]HeaderOrReference) - schema, err := r.Reflect(output, - openapi.WithOperationCtx(oc, true, openapi.InHeader), - jsonschema.InlineRefs, - jsonschema.PropertyNameMapping(mapping), - jsonschema.PropertyNameTag(tagHeader), - sanitizeDefName, - jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error { + schema, err := internal.ReflectResponseHeader(r.JSONSchemaReflector(), oc, cu, + func(params jsonschema.InterceptPropParams) error { if !params.Processed || len(params.Path) > 1 { // only top-level fields (including embedded). return nil } @@ -537,7 +513,7 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont } return nil - }), + }, ) if err != nil { return err diff --git a/openapi31/reflect_test.go b/openapi31/reflect_test.go index 2ba1d42..7db74e8 100644 --- a/openapi31/reflect_test.go +++ b/openapi31/reflect_test.go @@ -1135,3 +1135,56 @@ func TestReflector_AddOperation_defName(t *testing.T) { } }`, r.Spec) } + +func Test_Repro2(t *testing.T) { + oarefl := openapi31.NewReflector() + oarefl.JSONSchemaReflector().DefaultOptions = append(oarefl.JSONSchemaReflector().DefaultOptions, jsonschema.ProcessWithoutTags) + + { + var dummyIn struct { + ID int + } + + var dummyOut struct { + Done bool + } + + op, err := oarefl.NewOperationContext(http.MethodPost, "/postDelete") + if err != nil { + t.Fatal(err) + } + + op.AddReqStructure(dummyIn, openapi.WithContentType("application/json")) + op.AddRespStructure(dummyOut, openapi.WithHTTPStatus(200)) + if err = oarefl.AddOperation(op); err != nil { + t.Fatal(err) + } + } + + assertjson.EqMarshal(t, `{ + "openapi":"3.1.0","info":{"title":"","version":""}, + "paths":{ + "/postDelete":{ + "post":{ + "requestBody":{ + "content":{ + "application/json":{ + "schema":{"properties":{"ID":{"type":"integer"}},"type":"object"} + } + } + }, + "responses":{ + "200":{ + "description":"OK", + "content":{ + "application/json":{ + "schema":{"properties":{"Done":{"type":"boolean"}},"type":"object"} + } + } + } + } + } + } + } + }`, oarefl.SpecSchema()) +}