Skip to content

Commit

Permalink
Merge pull request #116 from danielgtaylor/fix-embedded-structs
Browse files Browse the repository at this point in the history
fix: incorrect schema for embedded structs
  • Loading branch information
danielgtaylor authored Aug 31, 2023
2 parents 35ab59f + f5a66d2 commit c456388
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 7 deletions.
57 changes: 50 additions & 7 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,47 @@ func SchemaFromField(registry Registry, parent reflect.Type, f reflect.StructFie
return fs
}

// fieldInfo stores information about a field, which may come from an
// embedded type. The `Parent` stores the field's direct parent.
type fieldInfo struct {
Parent reflect.Type
Field reflect.StructField
}

// getFields performs a breadth-first search for all fields including embedded
// ones. It may return multiple fields with the same name, the first of which
// represents the outer-most declaration.
func getFields(typ reflect.Type) []fieldInfo {
fields := make([]fieldInfo, 0, typ.NumField())
embedded := []reflect.StructField{}

for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if !f.IsExported() {
continue
}

if f.Anonymous {
embedded = append(embedded, f)
continue
}

fields = append(fields, fieldInfo{typ, f})
}

for _, f := range embedded {
newTyp := f.Type
for newTyp.Kind() == reflect.Ptr {
newTyp = newTyp.Elem()
}
if newTyp.Kind() == reflect.Struct {
fields = append(fields, getFields(newTyp)...)
}
}

return fields
}

func SchemaFromType(r Registry, t reflect.Type) *Schema {
s := Schema{}
t = deref(t)
Expand Down Expand Up @@ -388,12 +429,8 @@ func SchemaFromType(r Registry, t reflect.Type) *Schema {
requiredMap := map[string]bool{}
propNames := []string{}
props := map[string]*Schema{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)

if !f.IsExported() {
continue
}
for _, info := range getFields(t) {
f := info.Field

name := f.Name
omit := false
Expand All @@ -404,10 +441,16 @@ func SchemaFromType(r Registry, t reflect.Type) *Schema {
}
}
if name == "-" {
// This field is deliberately ignored.
continue
}
if props[name] != nil {
// This field was overridden by an ancestor type, so we
// should ignore it.
continue
}

fs := SchemaFromField(r, t, f)
fs := SchemaFromField(r, info.Parent, f)
if fs != nil {
props[name] = fs
propNames = append(propNames, name)
Expand Down
33 changes: 33 additions & 0 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import (
"github.com/stretchr/testify/assert"
)

type EmbeddedChild struct {
// This one should be ignored as it is overriden by `Embedded`.
Value string `json:"value" doc:"old doc"`
}

type Embedded struct {
EmbeddedChild
Value string `json:"value" doc:"new doc"`
}

func TestSchema(t *testing.T) {
bitSize := fmt.Sprint(bits.UintSize)

Expand Down Expand Up @@ -335,6 +345,29 @@ func TestSchema(t *testing.T) {
"additionalProperties": false
}`,
},
{
name: "field-embed",
input: struct {
// Because this is embedded, the fields should be merged into
// the parent object.
*Embedded
Value2 string `json:"value2"`
}{},
expected: `{
"type": "object",
"additionalProperties": false,
"required": ["value2", "value"],
"properties": {
"value": {
"type": "string",
"description": "new doc"
},
"value2": {
"type": "string"
}
}
}`,
},
{
name: "panic-bool",
input: struct {
Expand Down

0 comments on commit c456388

Please sign in to comment.