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
50 changes: 50 additions & 0 deletions fixtures/keynamed_with_ancestry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/invopop/jsonschema/key-named-with-ancestry",
"$ref": "#/$defs/KeyNamedWithAncestry",
"$defs": {
"KeyNamedWithAncestry": {
"properties": {
"root.Sub": {
"$ref": "#/$defs/KeyNamedWithAncestrySub"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"root.Sub"
]
},
"KeyNamedWithAncestrySub": {
"properties": {
"child-of-KeyNamedWithAncestry.RenamedByParent": {
"type": "string"
},
"child-of-KeyNamedWithAncestry.Nested": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/KeyNamedWithAncestryNested"
}
}
},
"additionalProperties": false,
"type": "object",
"required": [
"child-of-KeyNamedWithAncestry.RenamedByParent",
"child-of-KeyNamedWithAncestry.Nested"
]
},
"KeyNamedWithAncestryNested": {
"properties": {
"child-of-struct-in-map.RenamedByParent": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"child-of-struct-in-map.RenamedByParent"
]
}
}
}
88 changes: 61 additions & 27 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,16 @@ type Reflector struct {
// provided by the reflect package.
Namer func(reflect.Type) string

// KeyNamerWithAncestry allows customizing of key names based on the field ancestry.
// This allows to access the field's struct tags as well as the parent and grandparent types.
// If a json tag is present, KeyNamerWithAncestry will receive the tag's name as an argument, not the original key name.
// This takes precedence over KeyNamer if both are set.
KeyNamerWithAncestry func(FieldAncestry, string) string

// KeyNamer allows customizing of key names.
// The default is to use the key's name as is, or the json tag if present.
// If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name.
// This is ignored if KeyNamerWithAncestry is set.
KeyNamer func(string) string

// AdditionalFields allows adding structfields for a given type
Expand Down Expand Up @@ -186,7 +193,7 @@ func (r *Reflector) ReflectFromType(t reflect.Type) *Schema {
s := new(Schema)
definitions := Definitions{}
s.Definitions = definitions
bs := r.reflectTypeToSchemaWithID(definitions, t)
bs := r.reflectTypeToSchemaWithID(definitions, newFieldAncestry(t), t)
if r.ExpandedStruct {
if def := definitions[name]; def != nil {
*s = *def
Expand Down Expand Up @@ -248,7 +255,7 @@ func (r *Reflector) SetBaseSchemaID(id string) {
r.BaseSchemaID = ID(id)
}

func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, a FieldAncestry, t reflect.Type) *Schema {
id := r.lookupID(t)
if id != EmptyID {
return &Schema{
Expand All @@ -261,11 +268,11 @@ func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.
return def
}

return r.reflectTypeToSchemaWithID(definitions, t)
return r.reflectTypeToSchemaWithID(definitions, a, t)
}

func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type) *Schema {
s := r.reflectTypeToSchema(defs, t)
func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, a FieldAncestry, t reflect.Type) *Schema {
s := r.reflectTypeToSchema(defs, a, t)
if s != nil {
if r.Lookup != nil {
id := r.Lookup(t)
Expand All @@ -277,10 +284,10 @@ func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type)
return s
}

func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
func (r *Reflector) reflectTypeToSchema(definitions Definitions, a FieldAncestry, t reflect.Type) *Schema {
// only try to reflect non-pointers
if t.Kind() == reflect.Ptr {
return r.refOrReflectTypeToSchema(definitions, t.Elem())
return r.refOrReflectTypeToSchema(definitions, a, t.Elem())
}

// Check if the there is an alias method that provides an object
Expand All @@ -289,7 +296,7 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
v := reflect.New(t)
o := v.Interface().(aliasSchemaImpl)
t = reflect.TypeOf(o.JSONSchemaAlias())
return r.refOrReflectTypeToSchema(definitions, t)
return r.refOrReflectTypeToSchema(definitions, a, t)
}

// Do any pre-definitions exist?
Expand Down Expand Up @@ -327,13 +334,13 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)

switch t.Kind() {
case reflect.Struct:
r.reflectStruct(definitions, t, st)
r.reflectStruct(definitions, a, t, st)

case reflect.Slice, reflect.Array:
r.reflectSliceOrArray(definitions, t, st)
r.reflectSliceOrArray(definitions, a, t, st)

case reflect.Map:
r.reflectMap(definitions, t, st)
r.reflectMap(definitions, a, t, st)

case reflect.Interface:
// empty
Expand Down Expand Up @@ -397,7 +404,7 @@ func (r *Reflector) reflectSchemaExtend(definitions Definitions, t reflect.Type,
return s
}

func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema) {
func (r *Reflector) reflectSliceOrArray(definitions Definitions, a FieldAncestry, t reflect.Type, st *Schema) {
if t == rawMessageType {
return
}
Expand All @@ -419,33 +426,35 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type,
st.ContentEncoding = "base64"
} else {
st.Type = "array"
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
st.Items = r.refOrReflectTypeToSchema(definitions, a, t.Elem())
}
}

func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
func (r *Reflector) reflectMap(definitions Definitions, a FieldAncestry, t reflect.Type, st *Schema) {
r.addDefinition(definitions, t, st)

st.Type = "object"
if st.Description == "" {
st.Description = r.lookupComment(t, "")
}

a = a.addChild(t, reflect.StructField{})

switch t.Key().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
st.PatternProperties = map[string]*Schema{
"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem()),
"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, a, t.Elem()),
}
st.AdditionalProperties = FalseSchema
return
}
if t.Elem().Kind() != reflect.Interface {
st.AdditionalProperties = r.refOrReflectTypeToSchema(definitions, t.Elem())
st.AdditionalProperties = r.refOrReflectTypeToSchema(definitions, a, t.Elem())
}
}

// Reflects a struct to a JSON Schema type.
func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema) {
func (r *Reflector) reflectStruct(definitions Definitions, a FieldAncestry, t reflect.Type, s *Schema) {
// Handle special types
switch t {
case timeType: // date-time RFC section 7.3.1
Expand Down Expand Up @@ -477,11 +486,11 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc
}
}
if !ignored {
r.reflectStructFields(s, definitions, t)
r.reflectStructFields(s, definitions, a, t)
}
}

func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type) {
func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, a FieldAncestry, t reflect.Type) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
Expand All @@ -505,13 +514,13 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
customPropertyMethod = o.JSONSchemaProperty
}

handleField := func(f reflect.StructField) {
name, shouldEmbed, required, nullable := r.reflectFieldName(f)
handleField := func(a FieldAncestry, f reflect.StructField) {
name, shouldEmbed, required, nullable := r.reflectFieldName(a, f)
// if anonymous and exported type should be processed recursively
// current type should inherit properties of anonymous one
if name == "" {
if shouldEmbed {
r.reflectStructFields(st, definitions, f.Type)
r.reflectStructFields(st, definitions, a, f.Type)
}
return
}
Expand All @@ -520,9 +529,9 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
// the provided object's type instead of the field's type.
var property *Schema
if alias := customPropertyMethod(name); alias != nil {
property = r.refOrReflectTypeToSchema(definitions, reflect.TypeOf(alias))
property = r.refOrReflectTypeToSchema(definitions, a, reflect.TypeOf(alias))
} else {
property = r.refOrReflectTypeToSchema(definitions, f.Type)
property = r.refOrReflectTypeToSchema(definitions, a, f.Type)
}

property.structKeywordsFromTags(f, st, name)
Expand Down Expand Up @@ -552,12 +561,12 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r

for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
handleField(f)
handleField(a.addChild(t, f), f)
}
if r.AdditionalFields != nil {
if af := r.AdditionalFields(t); af != nil {
for _, sf := range af {
handleField(sf)
handleField(a.addChild(t, sf), sf)
}
}
}
Expand Down Expand Up @@ -1037,7 +1046,7 @@ func (r *Reflector) fieldNameTag() string {
return "json"
}

func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) {
func (r *Reflector) reflectFieldName(a FieldAncestry, f reflect.StructField) (string, bool, bool, bool) {
jsonTagString := f.Tag.Get(r.fieldNameTag())
jsonTags := strings.Split(jsonTagString, ",")

Expand Down Expand Up @@ -1083,13 +1092,38 @@ func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool,
if !f.Anonymous && f.PkgPath != "" {
// field not anonymous and not export has no export name
name = ""
} else if r.KeyNamerWithAncestry != nil {
name = r.KeyNamerWithAncestry(a, name)
} else if r.KeyNamer != nil {
name = r.KeyNamer(name)
}

return name, false, required, nullable
}

// FieldAncestry represents the ancestry of a field within nested structs.
// It is used to provide context when determining field names and other
// attributes based on the full path to a field.
type FieldAncestry struct {
Parent *FieldAncestry // Parent ancestry, or nil if root
Type reflect.Type // The type containing the field
Field reflect.StructField // The field itself
}

func newFieldAncestry(t reflect.Type) FieldAncestry {
return FieldAncestry{
Type: t,
}
}

func (fa FieldAncestry) addChild(t reflect.Type, f reflect.StructField) FieldAncestry {
return FieldAncestry{
Parent: &fa,
Type: t,
Field: f,
}
}

// UnmarshalJSON is used to parse a schema object or boolean.
func (t *Schema) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("true")) {
Expand Down
36 changes: 36 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,19 @@ type KeyNamed struct {
RenamedByComputation int `jsonschema_description:"Description was preserved"`
}

type KeyNamedWithAncestry struct {
Sub KeyNamedWithAncestrySub
}

type KeyNamedWithAncestrySub struct {
RenamedByParent string
Nested map[string]KeyNamedWithAncestryNested
}

type KeyNamedWithAncestryNested struct {
RenamedByParent string `jsonschema:"required"`
}

type SchemaExtendTestBase struct {
FirstName string `json:"FirstName"`
LastName string `json:"LastName"`
Expand Down Expand Up @@ -469,6 +482,29 @@ func TestSchemaGeneration(t *testing.T) {
return "unknown case"
},
}, "fixtures/keynamed.json"},
{&KeyNamedWithAncestry{}, &Reflector{
KeyNamerWithAncestry: func(fa FieldAncestry, s string) string {
if fa.Parent == nil {
return s
}

if fa.Parent.Parent == nil {
return "root." + s
}

if fa.Parent != nil && fa.Parent.Type.Kind() == reflect.Struct {
return "child-of-" + fa.Parent.Type.Name() + "." + s
}

if fa.Type != nil && fa.Type.Kind() == reflect.Struct {
if fa.Parent.Type.Kind() == reflect.Map {
return "child-of-struct-in-map." + s
}
}

return s
},
}, "fixtures/keynamed_with_ancestry.json"},
{MapType{}, &Reflector{}, "fixtures/map_type.json"},
{ArrayType{}, &Reflector{}, "fixtures/array_type.json"},
{SchemaExtendTest{}, &Reflector{}, "fixtures/custom_type_extend.json"},
Expand Down