Skip to content

Commit 3ba2005

Browse files
authored
add nested subschema defaults (#38)
Apply defaults from nested schemas.
1 parent 11a1c83 commit 3ba2005

File tree

2 files changed

+266
-3
lines changed

2 files changed

+266
-3
lines changed

jsonschema/validate.go

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -627,13 +627,12 @@ func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
627627
func (rs *Resolved) ApplyDefaults(instancep any) error {
628628
// TODO(jba): consider what defaults on top-level or array instances might mean.
629629
// TODO(jba): follow $ref and $dynamicRef
630-
// TODO(jba): apply defaults on sub-schemas to corresponding sub-instances.
631630
st := &state{rs: rs}
632631
return st.applyDefaults(reflect.ValueOf(instancep), rs.root)
633632
}
634633

635-
// Leave this as a potentially recursive helper function, because we'll surely want
636-
// to apply defaults on sub-schemas someday.
634+
// Recursive helper used by ApplyDefaults. Applies defaults on sub-schemas
635+
// of object properties recursively.
637636
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
638637
defer wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep)
639638

@@ -665,7 +664,42 @@ func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err err
665664
if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil {
666665
return err
667666
}
667+
// Recurse unconditionally; applyDefaults will only act on object-like values.
668+
if err := st.applyDefaults(lvalue, subschema); err != nil {
669+
return err
670+
}
668671
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
672+
} else if val.IsValid() {
673+
// Recurse into an existing sub-instance.
674+
// MapIndex returns a non-addressable value; copy into an addressable lvalue, recurse, then set back.
675+
lvalue := reflect.New(instance.Type().Elem())
676+
// Initialize the lvalue with current value.
677+
lvalue.Elem().Set(val)
678+
if err := st.applyDefaults(lvalue, subschema); err != nil {
679+
return err
680+
}
681+
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
682+
} else if schemaHasDefaultsInProperties(subschema) {
683+
// Property is missing, but descendants still have some defaults
684+
// Create an empty container and recurse to populate
685+
elemType := instance.Type().Elem()
686+
var child reflect.Value
687+
switch elemType.Kind() {
688+
case reflect.Interface:
689+
child = reflect.ValueOf(map[string]any{})
690+
case reflect.Map:
691+
child = reflect.MakeMap(elemType)
692+
case reflect.Struct:
693+
child = reflect.New(elemType).Elem()
694+
}
695+
if child.IsValid() {
696+
lvalue := reflect.New(elemType)
697+
lvalue.Elem().Set(child)
698+
if err := st.applyDefaults(lvalue, subschema); err != nil {
699+
return err
700+
}
701+
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
702+
}
669703
}
670704
case reflect.Struct:
671705
// If there is a default for this property, and the field exists but is zero,
@@ -674,6 +708,50 @@ func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err err
674708
if err := json.Unmarshal(subschema.Default, val.Addr().Interface()); err != nil {
675709
return err
676710
}
711+
// Recurse into newly set object to apply deeper defaults.
712+
if val.Kind() == reflect.Map {
713+
if val.IsNil() {
714+
val.Set(reflect.MakeMap(val.Type()))
715+
}
716+
if err := st.applyDefaults(val.Addr(), subschema); err != nil {
717+
return err
718+
}
719+
} else if val.Kind() == reflect.Struct {
720+
if err := st.applyDefaults(val.Addr(), subschema); err != nil {
721+
return err
722+
}
723+
}
724+
} else if val.IsValid() {
725+
// Recurse into existing sub-instance when object-like.
726+
switch val.Kind() {
727+
case reflect.Map:
728+
if val.IsNil() && schemaHasDefaultsInProperties(subschema) {
729+
val.Set(reflect.MakeMap(val.Type()))
730+
}
731+
if !val.IsNil() {
732+
if err := st.applyDefaults(val.Addr(), subschema); err != nil {
733+
return err
734+
}
735+
}
736+
case reflect.Struct:
737+
if err := st.applyDefaults(val.Addr(), subschema); err != nil {
738+
return err
739+
}
740+
case reflect.Pointer:
741+
et := val.Type().Elem()
742+
if (et.Kind() == reflect.Map || et.Kind() == reflect.Struct) && val.IsNil() && schemaHasDefaultsInProperties(subschema) {
743+
nv := reflect.New(et)
744+
if et.Kind() == reflect.Map {
745+
nv.Elem().Set(reflect.MakeMap(et))
746+
}
747+
val.Set(nv)
748+
}
749+
if !val.IsNil() && (et.Kind() == reflect.Map || et.Kind() == reflect.Struct) {
750+
if err := st.applyDefaults(val, subschema); err != nil {
751+
return err
752+
}
753+
}
754+
}
677755
}
678756
default:
679757
panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s",
@@ -684,6 +762,25 @@ func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err err
684762
return nil
685763
}
686764

765+
// schemaHasDefaultsInProperties reports whether s or any descendant schema under
766+
// its Properties contains a default. Only walks Properties to match ApplyDefaults semantics.
767+
func schemaHasDefaultsInProperties(s *Schema) bool {
768+
if s == nil {
769+
return false
770+
}
771+
if s.Default != nil {
772+
return true
773+
}
774+
if s.Properties != nil {
775+
for _, ss := range s.Properties {
776+
if schemaHasDefaultsInProperties(ss) {
777+
return true
778+
}
779+
}
780+
}
781+
return false
782+
}
783+
687784
// property returns the value of the property of v with the given name, or the invalid
688785
// reflect.Value if there is none.
689786
// If v is a map, the property is the value of the map whose key is name.

jsonschema/validate_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,172 @@ func TestApplyDefaults(t *testing.T) {
174174
}
175175
}
176176

177+
func TestApplyNestedDefaults(t *testing.T) {
178+
base := &Schema{
179+
Type: "object",
180+
Properties: map[string]*Schema{
181+
"A": {
182+
Type: "object",
183+
Properties: map[string]*Schema{
184+
"B": {Type: "string", Default: mustMarshal("foo")},
185+
},
186+
},
187+
},
188+
}
189+
// Variant where parent A has its own default object; recursion should still fill B.
190+
withParentDefault := &Schema{
191+
Type: "object",
192+
Properties: map[string]*Schema{
193+
"A": {
194+
Type: "object",
195+
Default: mustMarshal(map[string]any{"X": 1}),
196+
Properties: map[string]*Schema{
197+
"B": {Type: "string", Default: mustMarshal("foo")},
198+
},
199+
},
200+
},
201+
}
202+
203+
type Nested struct{ B string }
204+
type Root struct{ A Nested }
205+
type RootPtr struct{ A *Nested }
206+
type RootMap struct{ A map[string]any }
207+
type RootPtrMap struct{ A *map[string]any }
208+
209+
mapPtr := func(m map[string]any) *map[string]any { return &m }
210+
211+
for _, tc := range []struct {
212+
name string
213+
schema *Schema
214+
instancep any
215+
want any
216+
}{
217+
{
218+
name: "MapMissingParent",
219+
schema: base,
220+
instancep: &map[string]any{},
221+
want: map[string]any{"A": map[string]any{"B": "foo"}},
222+
},
223+
{
224+
name: "MapEmptyParent",
225+
schema: base,
226+
instancep: &map[string]any{"A": map[string]any{}},
227+
want: map[string]any{"A": map[string]any{"B": "foo"}},
228+
},
229+
{
230+
name: "MapParentHasDefaultObjectMissing",
231+
schema: withParentDefault,
232+
instancep: &map[string]any{},
233+
want: map[string]any{"A": map[string]any{"X": float64(1), "B": "foo"}},
234+
},
235+
{
236+
name: "MapParentHasDefaultObjectPresent",
237+
schema: withParentDefault,
238+
instancep: &map[string]any{"A": map[string]any{}},
239+
// Parent default is applied only when the property is missing, so
240+
// with the key present (even if empty), we apply only the nested defaults.
241+
want: map[string]any{"A": map[string]any{"B": "foo"}},
242+
},
243+
{
244+
name: "MapValueMapMissingParentTyped",
245+
schema: base,
246+
instancep: &map[string]map[string]any{},
247+
want: map[string]map[string]any{"A": {"B": "foo"}},
248+
},
249+
{
250+
name: "MapValueStructMissingParentTyped",
251+
schema: base,
252+
instancep: &map[string]Nested{},
253+
want: map[string]Nested{"A": {B: "foo"}},
254+
},
255+
{
256+
name: "StructZeroValueParent",
257+
schema: base,
258+
instancep: &Root{},
259+
want: Root{A: Nested{B: "foo"}},
260+
},
261+
{
262+
name: "StructZeroValueParentWithParentDefault",
263+
schema: withParentDefault,
264+
instancep: &Root{},
265+
want: Root{A: Nested{B: "foo"}},
266+
},
267+
{
268+
name: "StructPointerNilParent",
269+
schema: base,
270+
instancep: &RootPtr{},
271+
want: RootPtr{A: &Nested{B: "foo"}},
272+
},
273+
{
274+
name: "StructPresentNonzeroChildPreserved",
275+
schema: base,
276+
instancep: &Root{A: Nested{B: "bar"}},
277+
want: Root{A: Nested{B: "bar"}},
278+
},
279+
{
280+
name: "StructPointerNonNilChildPreserved",
281+
schema: base,
282+
instancep: &RootPtr{A: &Nested{B: "bar"}},
283+
want: RootPtr{A: &Nested{B: "bar"}},
284+
},
285+
{
286+
name: "StructMapNilParent",
287+
schema: base,
288+
instancep: &RootMap{},
289+
want: RootMap{A: map[string]any{"B": "foo"}},
290+
},
291+
{
292+
name: "StructMapParentDefaultObjectMissing",
293+
schema: withParentDefault,
294+
instancep: &RootMap{},
295+
want: RootMap{A: map[string]any{"X": float64(1), "B": "foo"}},
296+
},
297+
{
298+
name: "StructMapParentDefaultObjectPresent",
299+
schema: withParentDefault,
300+
instancep: &RootMap{A: map[string]any{}},
301+
want: RootMap{A: map[string]any{"B": "foo"}},
302+
},
303+
{
304+
name: "StructPtrMapNilParent",
305+
schema: base,
306+
instancep: &RootPtrMap{},
307+
want: RootPtrMap{A: mapPtr(map[string]any{"B": "foo"})},
308+
},
309+
{
310+
name: "StructMapNilParentWithNullParentDefault",
311+
schema: &Schema{
312+
Type: "object",
313+
Properties: map[string]*Schema{
314+
"A": {
315+
// Default null exercises map allocation in struct subschemas
316+
Default: mustMarshal(nil),
317+
Properties: map[string]*Schema{
318+
"B": {Type: "string", Default: mustMarshal("foo")},
319+
},
320+
},
321+
},
322+
},
323+
instancep: &RootMap{},
324+
want: RootMap{A: map[string]any{"B": "foo"}},
325+
},
326+
} {
327+
t.Run(tc.name, func(t *testing.T) {
328+
rs, err := tc.schema.Resolve(&ResolveOptions{ValidateDefaults: true})
329+
if err != nil {
330+
t.Fatal(err)
331+
}
332+
if err := rs.ApplyDefaults(tc.instancep); err != nil {
333+
t.Fatal(err)
334+
}
335+
got := reflect.ValueOf(tc.instancep).Elem().Interface()
336+
if !reflect.DeepEqual(got, tc.want) {
337+
t.Errorf("nested defaults:\n got %#v\n want %#v", got, tc.want)
338+
}
339+
})
340+
}
341+
}
342+
177343
func TestStructInstance(t *testing.T) {
178344
instance := struct {
179345
I int

0 commit comments

Comments
 (0)