Skip to content
Open
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
25 changes: 24 additions & 1 deletion openapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,9 @@ func (g *Generator) newSchemaFromStructField(sf reflect.StructField, required bo
if sor == nil {
return nil
}

sor = g.addExtensions(sor, sf)

// Get the underlying schema, it may be a reference
// to a component, and update its fields using the
// informations in the struct field tags.
Expand Down Expand Up @@ -834,6 +837,26 @@ func (g *Generator) newSchemaFromStructField(sf reflect.StructField, required bo
return sor
}

func (g *Generator) addExtensions(sor *SchemaOrRef, sf reflect.StructField) *SchemaOrRef {
// Check if the json field has the omitempty tag.
jsonTag := sf.Tag.Get("json")
hasOmitEmpty := strings.Contains(jsonTag, "omitempty")

if sor.Schema != nil {
if sor.Schema.Extensions == nil {
sor.Schema.Extensions = make(map[string]interface{})
}
sor.Schema.Extensions["x-omitempty"] = hasOmitEmpty
} else if sor.Reference != nil {
if sor.Reference.Extensions == nil {
sor.Reference.Extensions = make(map[string]interface{})
}
sor.Reference.Extensions["x-omitempty"] = hasOmitEmpty
}

return sor
}

func (g *Generator) enumFromStructField(sf reflect.StructField, fname string, parent reflect.Type) []interface{} {
var enum []interface{}

Expand Down Expand Up @@ -1255,7 +1278,7 @@ func fieldNameFromTag(sf reflect.StructField, tagName string) string {
return name
}

/// parseExampleValue is used to transform the string representation of the example value to the correct type.
// / parseExampleValue is used to transform the string representation of the example value to the correct type.
func parseExampleValue(t reflect.Type, value string) (interface{}, error) {
// If the type implements Exampler use the ParseExample method to create the example
i, ok := reflect.New(t).Interface().(Exampler)
Expand Down
8 changes: 4 additions & 4 deletions openapi/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package openapi
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"os"
"reflect"
"strconv"
"testing"
Expand Down Expand Up @@ -213,7 +213,7 @@ func TestSchemaFromComplex(t *testing.T) {
t.Error(err)
}
// see testdata/X.json.
expected, err := ioutil.ReadFile("../testdata/schemas/X.json")
expected, err := os.ReadFile("../testdata/schemas/X.json")
if err != nil {
t.Error(err)
}
Expand All @@ -234,7 +234,7 @@ func TestSchemaFromComplex(t *testing.T) {
t.Error(err)
}
// see testdata/Y.json.
expected, err = ioutil.ReadFile("../testdata/schemas/Y.json")
expected, err = os.ReadFile("../testdata/schemas/Y.json")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -550,7 +550,7 @@ func TestAddOperation(t *testing.T) {
t.Error(err)
}
// see testdata/schemas/path-item.json.
expected, err := ioutil.ReadFile("../testdata/schemas/path-item.json")
expected, err := os.ReadFile("../testdata/schemas/path-item.json")
if err != nil {
t.Error(err)
}
Expand Down
93 changes: 92 additions & 1 deletion openapi/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,26 @@ type PathItem struct {
// other components in the specification, internally and
// externally.
type Reference struct {
Ref string `json:"$ref" yaml:"$ref"`
Ref string `json:"$ref" yaml:"$ref"`
Extensions map[string]interface{} `json:"-" yaml:"-"`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get the rationale for this field addition.
Extensions should only be present on the Schema struct. It can then be used inline or be references through the spec components.

}

func (r *Reference) MarshalJSON() ([]byte, error) {
if r == nil {
return []byte("{}"), nil
}

m := map[string]interface{}{
"$ref": r.Ref,
}

if r.Extensions != nil {
for k, v := range r.Extensions {
m[k] = v
}
}

return json.Marshal(m)
}

// Parameter describes a single operation parameter.
Expand Down Expand Up @@ -124,6 +143,16 @@ func (por *ParameterOrRef) MarshalYAML() (interface{}, error) {
return por.Reference, nil
}

func (p *ParameterOrRef) MarshalJSON() ([]byte, error) {
if p.Parameter != nil {
return json.Marshal(p.Parameter)
}
if p.Reference != nil {
return json.Marshal(p.Reference)
}
return []byte("{}"), nil
}

// RequestBody represents a request body.
type RequestBody struct {
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Expand All @@ -146,6 +175,22 @@ func (sor *SchemaOrRef) MarshalYAML() (interface{}, error) {
return sor.Reference, nil
}

func (sor *SchemaOrRef) MarshalJSON() ([]byte, error) {
if sor == nil {
return nil, nil
}

if sor.Schema != nil {
return json.Marshal(sor.Schema)
}

if sor.Reference != nil {
return json.Marshal(sor.Reference)
}

return []byte("{}"), nil
}

// Schema represents the definition of input and output data
// types of the API.
type Schema struct {
Expand Down Expand Up @@ -184,6 +229,35 @@ type Schema struct {
Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"`
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`

Extensions map[string]interface{} `json:"-" yaml:"-"`
}

func (s *Schema) MarshalJSON() ([]byte, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I get it right, you're trying to simulate the new omitnil JSON field tag, to avoid marshaling the Extensions field it it's empty ? What the purpose.
omitnil could be used (from go1.24), and for previous versions, it doesn't really matter if we marshal extensions: [].

if s == nil {
return nil, nil
}

type Alias Schema
base, err := json.Marshal((*Alias)(s))
if err != nil {
return nil, err
}

if len(s.Extensions) == 0 {
return base, nil
}

var baseMap map[string]interface{}
if err := json.Unmarshal(base, &baseMap); err != nil {
return nil, err
}

for k, v := range s.Extensions {
baseMap[k] = v
}

return json.Marshal(baseMap)
}

// Operation describes an API operation on a path.
Expand Down Expand Up @@ -271,6 +345,16 @@ func (ror *ResponseOrRef) MarshalYAML() (interface{}, error) {
return ror.Reference, nil
}

func (r *ResponseOrRef) MarshalJSON() ([]byte, error) {
if r.Response != nil {
return json.Marshal(r.Response)
}
if r.Reference != nil {
return json.Marshal(r.Reference)
}
return []byte("{}"), nil
}

// Response describes a single response from an API.
type Response struct {
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Expand Down Expand Up @@ -317,6 +401,13 @@ func (mtor *MediaTypeOrRef) MarshalYAML() (interface{}, error) {
return mtor.Reference, nil
}

func (m *MediaTypeOrRef) MarshalJSON() ([]byte, error) {
if m.MediaType != nil {
return json.Marshal(m.MediaType)
}
return []byte("{}"), nil
}

// MediaType represents the type of a media.
type MediaType struct {
Schema *SchemaOrRef `json:"schema" yaml:"schema"`
Expand Down
4 changes: 2 additions & 2 deletions openapi/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package openapi

import (
"encoding/json"
"io/ioutil"
"os"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -52,7 +52,7 @@ func TestSchemaValidation(t *testing.T) {
t.Error(err)
}
// see testdata/validation/len.json.
expected, err := ioutil.ReadFile("../testdata/schemas/validation.json")
expected, err := os.ReadFile("../testdata/schemas/validation.json")
if err != nil {
t.Error(err)
}
Expand Down
88 changes: 54 additions & 34 deletions testdata/schemas/X.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,114 @@
"type": "object",
"properties": {
"A": {
"type": "string"
"type": "string",
"x-omitempty": false
},
"B": {
"type": "integer",
"format": "int32",
"nullable": true
"nullable": true,
"type": "integer",
"x-omitempty": false
},
"C": {
"deprecated": true,
"type": "boolean",
"deprecated": true
"x-omitempty": false
},
"D": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Y"
}
},
"type": "array",
"x-omitempty": false
},
"E": {
"type": "array",
"items": {
"$ref": "#/components/schemas/XXX"
},
"maxItems": 3,
"minItems": 3
"minItems": 3,
"type": "array",
"x-omitempty": false
},
"F": {
"$ref": "#/components/schemas/XXX"
"$ref": "#/components/schemas/XXX",
"x-omitempty": false
},
"G": {
"$ref": "#/components/schemas/Y"
"$ref": "#/components/schemas/Y",
"x-omitempty": false
},
"H": {
"format": "float",
"type": "number",
"format": "float"
"x-omitempty": false
},
"I": {
"format": "date",
"type": "string",
"format": "date"
"x-omitempty": false
},
"J": {
"type": "integer",
"format": "int32",
"nullable": true
"nullable": true,
"type": "integer",
"x-omitempty": false
},
"K": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Y"
}
},
"type": "object",
"x-omitempty": false
},
"N": {
"type": "object",
"properties": {
"Na": {
"type": "string"
"type": "string",
"x-omitempty": false
},
"Nb": {
"type": "string"
"type": "string",
"x-omitempty": false
},
"Nc": {
"format": "duration",
"type": "string",
"format": "duration"
"x-omitempty": false
}
}
},
"type": "object",
"x-omitempty": false
},
"S": {
"NI": {
"format": "int32",
"nullable": true,
"type": "integer",
"format": "int32"
},
"nnNnnN":{
"type":"string"
},
"data": {
"$ref": "#/components/schemas/V"
"x-omitempty": false
},
"NS": {
"nullable": true,
"type": "string"
"type": "string",
"x-omitempty": false
},
"NI" : {
"nullable": true,
"S": {
"format": "int32",
"type": "integer",
"format": "int32"
"x-omitempty": false
},
"data": {
"$ref": "#/components/schemas/V",
"x-omitempty": false
},
"nnNnnN": {
"type": "string",
"x-omitempty": false
}
},
"required": [
"A",
"H",
"K"
]
}
}
Loading