From 5d8158f127b1c78ec8606cbcf1ac479c55914602 Mon Sep 17 00:00:00 2001 From: Claudio Beatrice Date: Sat, 26 Oct 2024 11:20:25 +0200 Subject: [PATCH] Additional Properties do not get unmarshalled (#278) * feat: introduce support for unmarshalling additional properties in json * feat: introduce support for unmarshalling additional properties in yaml * chore: fix linting issues --- go.sum | 4 +- go.work.sum | 3 +- pkg/generator/generate.go | 3 +- pkg/generator/json_formatter.go | 24 +++- pkg/generator/schema_generator.go | 9 +- pkg/generator/utils.go | 2 + pkg/generator/yaml_formatter.go | 21 +++ .../arrayAdditionalProperties.go | 13 +- .../boolAdditionalProperties.go | 13 +- .../intAdditionalProperties.go | 13 +- .../numberAdditionalProperties.go | 13 +- .../objectAdditionalProperties.go | 13 +- .../objectWithPropsAdditionalProperties.go | 44 ++++++ .../objectWithPropsAdditionalProperties.json | 24 ++++ .../stringAdditionalProperties.go | 13 +- .../gopkgYAMLv3AdditionalProperties.go | 71 ++++++++++ .../gopkgYAMLv3AdditionalProperties.json | 24 ++++ .../gopkgYAMLv3AdditionalProperties.yaml | 6 + tests/data/regressions/issue51/issue51.go | 2 +- tests/generation_test.go | 8 ++ tests/go.mod | 4 + tests/go.sum | 11 +- tests/unmarshal_test.go | 129 ++++++++++++++++++ 23 files changed, 448 insertions(+), 19 deletions(-) create mode 100644 tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.go create mode 100644 tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.json create mode 100644 tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.go create mode 100644 tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.json create mode 100644 tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.yaml diff --git a/go.sum b/go.sum index e073bcf6..a7f64a1c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,7 +27,6 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -39,7 +37,6 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0BqZYGxvYaXvFGUbCmPGy8DM7qWJJiIQ= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -57,6 +54,7 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum index 61fbef95..6716c330 100644 --- a/go.work.sum +++ b/go.work.sum @@ -4,9 +4,8 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/pkg/generator/generate.go b/pkg/generator/generate.go index d17b9618..4e071496 100644 --- a/pkg/generator/generate.go +++ b/pkg/generator/generate.go @@ -175,7 +175,8 @@ func (g *Generator) findOutputFileForSchemaID(id string) (*output, error) { func (g *Generator) beginOutput( id string, - outputName, packageName string, + outputName, + packageName string, ) (*output, error) { if packageName == "" { return nil, fmt.Errorf("%w: %q", errMapURIToPackageName, id) diff --git a/pkg/generator/json_formatter.go b/pkg/generator/json_formatter.go index 44eeb8e7..0a28cfa6 100644 --- a/pkg/generator/json_formatter.go +++ b/pkg/generator/json_formatter.go @@ -36,8 +36,7 @@ func (jf *jsonFormatter) generate(declType codegen.TypeDecl, validators []valida if forceBefore || len(beforeValidators) != 0 { out.Printlnf("var %s map[string]interface{}", varNameRawMap) - out.Printlnf("if err := %s.Unmarshal(b, &%s); err != nil { return err }", - formatJSON, varNameRawMap) + out.Printlnf("if err := %s.Unmarshal(b, &%s); err != nil { return err }", formatJSON, varNameRawMap) } for _, v := range beforeValidators { @@ -53,6 +52,27 @@ func (jf *jsonFormatter) generate(declType codegen.TypeDecl, validators []valida v.generate(out) } + if structType, ok := declType.Type.(*codegen.StructType); ok { + for _, f := range structType.Fields { + if f.Name == additionalProperties { + out.Printlnf("st := reflect.TypeOf(Plain{})") + out.Printlnf("for i := range st.NumField() {") + out.Indent(1) + out.Printlnf("delete(raw, st.Field(i).Name)") + out.Printlnf("delete(raw, strings.Split(st.Field(i).Tag.Get(\"json\"), \",\")[0])") + out.Indent(-1) + out.Printlnf("}") + out.Printlnf("if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil {") + out.Indent(1) + out.Printlnf("return err") + out.Indent(-1) + out.Printlnf("}") + + break + } + } + } + out.Printlnf("*j = %s(%s)", declType.Name, varNamePlainStruct) out.Printlnf("return nil") out.Indent(-1) diff --git a/pkg/generator/schema_generator.go b/pkg/generator/schema_generator.go index e89a777f..d7c6072c 100644 --- a/pkg/generator/schema_generator.go +++ b/pkg/generator/schema_generator.go @@ -240,6 +240,12 @@ func (g *schemaGenerator) generateDeclaredType( for _, f := range structType.Fields { if f.DefaultValue != nil { + if f.Name == additionalProperties { + g.output.file.Package.AddImport("reflect", "") + g.output.file.Package.AddImport("strings", "") + g.output.file.Package.AddImport("github.com/go-viper/mapstructure/v2", "") + } + validators = append(validators, &defaultValidator{ jsonName: f.JSONName, fieldName: f.Name, @@ -633,10 +639,11 @@ func (g *schemaGenerator) generateStructType( structType.AddField( codegen.StructField{ - Name: "AdditionalProperties", + Name: additionalProperties, DefaultValue: defaultValue, SchemaType: &schemas.Type{}, Type: fieldType, + Tags: "mapstructure:\",remain\"", }, ) } diff --git a/pkg/generator/utils.go b/pkg/generator/utils.go index 77aff3e1..a40e6176 100644 --- a/pkg/generator/utils.go +++ b/pkg/generator/utils.go @@ -7,6 +7,8 @@ import ( "github.com/atombender/go-jsonschema/pkg/schemas" ) +const additionalProperties = "AdditionalProperties" + func sortPropertiesByName(props map[string]*schemas.Type) []string { names := make([]string, 0, len(props)) for name := range props { diff --git a/pkg/generator/yaml_formatter.go b/pkg/generator/yaml_formatter.go index 1e374e23..c2d97a79 100644 --- a/pkg/generator/yaml_formatter.go +++ b/pkg/generator/yaml_formatter.go @@ -53,6 +53,27 @@ func (yf *yamlFormatter) generate(declType codegen.TypeDecl, validators []valida v.generate(out) } + if structType, ok := declType.Type.(*codegen.StructType); ok { + for _, f := range structType.Fields { + if f.Name == "AdditionalProperties" { + out.Printlnf("st := reflect.TypeOf(Plain{})") + out.Printlnf("for i := range st.NumField() {") + out.Indent(1) + out.Printlnf("delete(raw, st.Field(i).Name)") + out.Printlnf("delete(raw, strings.Split(st.Field(i).Tag.Get(\"json\"), \",\")[0])") + out.Indent(-1) + out.Printlnf("}") + out.Printlnf("if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil {") + out.Indent(1) + out.Printlnf("return err") + out.Indent(-1) + out.Printlnf("}") + + break + } + } + } + out.Printlnf("*j = %s(%s)", declType.Name, varNamePlainStruct) out.Printlnf("return nil") out.Indent(-1) diff --git a/tests/data/core/additionalProperties/arrayAdditionalProperties.go b/tests/data/core/additionalProperties/arrayAdditionalProperties.go index e11133c4..61a71896 100644 --- a/tests/data/core/additionalProperties/arrayAdditionalProperties.go +++ b/tests/data/core/additionalProperties/arrayAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type ArrayAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string][]interface{} + AdditionalProperties map[string][]interface{} `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *ArrayAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string][]interface{}{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = ArrayAdditionalProperties(plain) return nil } diff --git a/tests/data/core/additionalProperties/boolAdditionalProperties.go b/tests/data/core/additionalProperties/boolAdditionalProperties.go index 3a675118..2ad1a999 100644 --- a/tests/data/core/additionalProperties/boolAdditionalProperties.go +++ b/tests/data/core/additionalProperties/boolAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type BoolAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string]bool + AdditionalProperties map[string]bool `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *BoolAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string]bool{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = BoolAdditionalProperties(plain) return nil } diff --git a/tests/data/core/additionalProperties/intAdditionalProperties.go b/tests/data/core/additionalProperties/intAdditionalProperties.go index cbd2b6c6..7ac80648 100644 --- a/tests/data/core/additionalProperties/intAdditionalProperties.go +++ b/tests/data/core/additionalProperties/intAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type IntAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string]int + AdditionalProperties map[string]int `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *IntAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string]int{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = IntAdditionalProperties(plain) return nil } diff --git a/tests/data/core/additionalProperties/numberAdditionalProperties.go b/tests/data/core/additionalProperties/numberAdditionalProperties.go index 018cebbe..56b6147d 100644 --- a/tests/data/core/additionalProperties/numberAdditionalProperties.go +++ b/tests/data/core/additionalProperties/numberAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type NumberAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string]float64 + AdditionalProperties map[string]float64 `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *NumberAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string]float64{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = NumberAdditionalProperties(plain) return nil } diff --git a/tests/data/core/additionalProperties/objectAdditionalProperties.go b/tests/data/core/additionalProperties/objectAdditionalProperties.go index b300de35..24d75024 100644 --- a/tests/data/core/additionalProperties/objectAdditionalProperties.go +++ b/tests/data/core/additionalProperties/objectAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type ObjectAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string]interface{} + AdditionalProperties map[string]interface{} `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *ObjectAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string]interface{}{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = ObjectAdditionalProperties(plain) return nil } diff --git a/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.go b/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.go new file mode 100644 index 00000000..d27efcd3 --- /dev/null +++ b/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.go @@ -0,0 +1,44 @@ +// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. + +package test + +import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" + +type ObjectWithPropsAdditionalProperties struct { + // Bar corresponds to the JSON schema field "bar". + Bar *string `json:"bar,omitempty" yaml:"bar,omitempty" mapstructure:"bar,omitempty"` + + // Foo corresponds to the JSON schema field "foo". + Foo *string `json:"foo,omitempty" yaml:"foo,omitempty" mapstructure:"foo,omitempty"` + + AdditionalProperties map[string]interface{} `mapstructure:",remain"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ObjectWithPropsAdditionalProperties) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + type Plain ObjectWithPropsAdditionalProperties + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if v, ok := raw[""]; !ok || v == nil { + plain.AdditionalProperties = map[string]interface{}{} + } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } + *j = ObjectWithPropsAdditionalProperties(plain) + return nil +} diff --git a/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.json b/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.json new file mode 100644 index 00000000..1b671990 --- /dev/null +++ b/tests/data/core/additionalProperties/objectWithPropsAdditionalProperties.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://example.com/empty_object_properties", + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "property1": { + "type": "string" + }, + "property2": { + "type": "number" + } + } + }, + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + } +} diff --git a/tests/data/core/additionalProperties/stringAdditionalProperties.go b/tests/data/core/additionalProperties/stringAdditionalProperties.go index f00064f2..e709f40e 100644 --- a/tests/data/core/additionalProperties/stringAdditionalProperties.go +++ b/tests/data/core/additionalProperties/stringAdditionalProperties.go @@ -3,12 +3,15 @@ package test import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import "reflect" +import "strings" type StringAdditionalProperties struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties map[string]string + AdditionalProperties map[string]string `mapstructure:",remain"` } // UnmarshalJSON implements json.Unmarshaler. @@ -25,6 +28,14 @@ func (j *StringAdditionalProperties) UnmarshalJSON(b []byte) error { if v, ok := raw[""]; !ok || v == nil { plain.AdditionalProperties = map[string]string{} } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } *j = StringAdditionalProperties(plain) return nil } diff --git a/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.go b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.go new file mode 100644 index 00000000..c4a44ac2 --- /dev/null +++ b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.go @@ -0,0 +1,71 @@ +// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. + +package test + +import "encoding/json" +import "github.com/go-viper/mapstructure/v2" +import yaml "gopkg.in/yaml.v3" +import "reflect" +import "strings" + +type GopkgYAMLv3AdditionalProperties struct { + // Bar corresponds to the JSON schema field "bar". + Bar *string `json:"bar,omitempty" yaml:"bar,omitempty" mapstructure:"bar,omitempty"` + + // Foo corresponds to the JSON schema field "foo". + Foo *string `json:"foo,omitempty" yaml:"foo,omitempty" mapstructure:"foo,omitempty"` + + AdditionalProperties map[string]interface{} `mapstructure:",remain"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *GopkgYAMLv3AdditionalProperties) UnmarshalJSON(b []byte) error { + var raw map[string]interface{} + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + type Plain GopkgYAMLv3AdditionalProperties + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + if v, ok := raw[""]; !ok || v == nil { + plain.AdditionalProperties = map[string]interface{}{} + } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } + *j = GopkgYAMLv3AdditionalProperties(plain) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *GopkgYAMLv3AdditionalProperties) UnmarshalYAML(value *yaml.Node) error { + var raw map[string]interface{} + if err := value.Decode(&raw); err != nil { + return err + } + type Plain GopkgYAMLv3AdditionalProperties + var plain Plain + if err := value.Decode(&plain); err != nil { + return err + } + if v, ok := raw[""]; !ok || v == nil { + plain.AdditionalProperties = map[string]interface{}{} + } + st := reflect.TypeOf(Plain{}) + for i := range st.NumField() { + delete(raw, st.Field(i).Name) + delete(raw, strings.Split(st.Field(i).Tag.Get("json"), ",")[0]) + } + if err := mapstructure.Decode(raw, &plain.AdditionalProperties); err != nil { + return err + } + *j = GopkgYAMLv3AdditionalProperties(plain) + return nil +} diff --git a/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.json b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.json new file mode 100644 index 00000000..1901725d --- /dev/null +++ b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://example.com/empty_object_properties", + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "property1": { + "type": "string" + }, + "property2": { + "type": "number" + } + } + }, + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + } +} diff --git a/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.yaml b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.yaml new file mode 100644 index 00000000..089ac90f --- /dev/null +++ b/tests/data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.yaml @@ -0,0 +1,6 @@ +--- +foo: "example1" +bar: "example2" +baz: + property1: "hello" + property2: 123 diff --git a/tests/data/regressions/issue51/issue51.go b/tests/data/regressions/issue51/issue51.go index c2a91b77..9b9c4f74 100644 --- a/tests/data/regressions/issue51/issue51.go +++ b/tests/data/regressions/issue51/issue51.go @@ -6,5 +6,5 @@ type Issue51 struct { // Name corresponds to the JSON schema field "name". Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` - AdditionalProperties interface{} + AdditionalProperties interface{} `mapstructure:",remain"` } diff --git a/tests/generation_test.go b/tests/generation_test.go index cf4d72a4..a8ad3fa1 100644 --- a/tests/generation_test.go +++ b/tests/generation_test.go @@ -155,6 +155,14 @@ func TestExtraImportsYAML(t *testing.T) { testExampleFile(t, cfg, "./data/extraImports/gopkgYAMLv3/gopkgYAMLv3.json") } +func TestExtraImportsYAMLAdditionalProperties(t *testing.T) { + t.Parallel() + + cfg := basicConfig + cfg.ExtraImports = true + testExampleFile(t, cfg, "./data/extraImports/gopkgYAMLv3AdditionalProperties/gopkgYAMLv3AdditionalProperties.json") +} + func TestMinSizeInt(t *testing.T) { t.Parallel() diff --git a/tests/go.mod b/tests/go.mod index 6313d3a7..b9a6e90d 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -12,16 +12,20 @@ replace ( require ( github.com/atombender/go-jsonschema v0.16.0 github.com/atombender/go-jsonschema/tests/helpers/other v0.0.0-20240909221408-bcba1cdc5eb2 + github.com/go-viper/mapstructure/v2 v2.1.0 + github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.17.0 // indirect github.com/goccy/go-yaml v1.12.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/sys v0.25.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index 614a53ca..7b0ff71c 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -1,5 +1,6 @@ -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -8,6 +9,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -23,12 +26,14 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0BqZYGxvYaXvFGUbCmPGy8DM7qWJJiIQ= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= diff --git a/tests/unmarshal_test.go b/tests/unmarshal_test.go index 9d5f5254..2fc9b3ce 100644 --- a/tests/unmarshal_test.go +++ b/tests/unmarshal_test.go @@ -1,14 +1,17 @@ package tests_test import ( + "encoding/json" "fmt" "os" "reflect" "strings" "testing" + "github.com/stretchr/testify/assert" yamlv3 "gopkg.in/yaml.v3" + testAdditionalProperties "github.com/atombender/go-jsonschema/tests/data/core/additionalProperties" test "github.com/atombender/go-jsonschema/tests/data/extraImports/gopkgYAMLv3" ) @@ -70,6 +73,132 @@ func TestYamlV3UnmarshalInvalidEnum(t *testing.T) { } } +func TestJSONUnmarshalAdditionalProperties(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + json string + target json.Unmarshaler + assertFn func(target json.Unmarshaler) + }{ + { + desc: "array", + json: `{ + "name": "hello world", + "property1": ["one", "two"], + "property2": [3, 4] + }`, + target: &testAdditionalProperties.ArrayAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.ArrayAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string][]any{"property1": {"one", "two"}, "property2": {3.0, 4.0}}, addProps) + }, + }, + { + desc: "bool", + json: `{ + "name": "hello world", + "property1": true, + "property2": false + }`, + target: &testAdditionalProperties.BoolAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.BoolAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]bool{"property1": true, "property2": false}, addProps) + }, + }, + { + desc: "int", + json: `{ + "name": "hello world", + "property1": 1, + "property2": 2 + }`, + target: &testAdditionalProperties.IntAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.IntAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]int{"property1": 1, "property2": 2}, addProps) + }, + }, + { + desc: "number", + json: `{ + "name": "hello world", + "property1": 1.1, + "property2": 2.3 + }`, + target: &testAdditionalProperties.NumberAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.NumberAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]float64{"property1": 1.1, "property2": 2.3}, addProps) + }, + }, + { + desc: "object", + json: `{ + "name": "hello world", + "surname": { + "hello": 1.1, + "world": "what's up?" + } + }`, + target: &testAdditionalProperties.ObjectAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.ObjectAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]any{"surname": map[string]any{"hello": 1.1, "world": "what's up?"}}, addProps) + }, + }, + { + desc: "object with props", + json: `{ + "foo": "foo value", + "bar": "bar value", + "baz": { + "property1": "hello", + "property2": 123 + } + }`, + target: &testAdditionalProperties.ObjectWithPropsAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.ObjectWithPropsAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]any{"baz": map[string]any{"property1": "hello", "property2": 123.0}}, addProps) + }, + }, + { + desc: "string", + json: `{ + "name": "hello world", + "property1": "hello", + "property2": "world" + }`, + target: &testAdditionalProperties.StringAdditionalProperties{}, + assertFn: func(target json.Unmarshaler) { + addProps := target.(*testAdditionalProperties.StringAdditionalProperties).AdditionalProperties + + assert.Equal(t, map[string]string{"property1": "hello", "property2": "world"}, addProps) + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + if err := tC.target.UnmarshalJSON([]byte(tC.json)); err != nil { + t.Fatalf("unmarshal error: %s", err) + } + + tC.assertFn(tC.target) + }) + } +} + func formatGopkgYAMLv3(v test.GopkgYAMLv3) string { return fmt.Sprintf( "GopkgYAMLv3{MyString: %s, MyNumber: %f, MyInteger: %d, MyBoolean: %t, MyNull: %v, MyEnum: %v}",