internal/mcp/jsonschema: apply schema defaults to an instance

This CL handles only the limited case of defaults on properties.

This is just a start: I need to think more about defaults on other
schema keywords, and I need to set defaults recursively.

As part of this work, I realized that the Schema.Default field is
more usable when represented as a json.RawMessage instead of a *any.

Change-Id: Iedcedb405b52607a312dbd895fa61e7f7f94c6bc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/676938
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-27 07:08:57 -04:00 • committed by Gopher Robot
commit c5a3591742

View file

@ -5,6 +5,7 @@
package jsonschema
import (
"encoding/json"
"fmt"
"hash/maphash"
"iter"
@ -47,7 +48,11 @@ func (rs *Resolved) validateDefaults() error {
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", s)
}
if s.Default != nil {
if err := st.validate(reflect.ValueOf(*s.Default), s, nil); err != nil {
var d any
if err := json.Unmarshal(s.Default, &d); err != nil {
fmt.Errorf("unmarshaling default value of schema %s: %w", s, err)
}
if err := st.validate(reflect.ValueOf(d), s, nil); err != nil {
return err
}
}
@ -68,6 +73,7 @@ type state struct {
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
defer wrapf(&err, "validating %s", schema)
// Maintain a stack for dynamic schema resolution.
st.stack = append(st.stack, schema) // push
defer func() {
st.stack = st.stack[:len(st.stack)-1] // pop
@ -536,6 +542,113 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
return nil
}
// resolveDynamicRef returns the schema referred to by the argument schema's
// $dynamicRef value.
// It returns an error if the dynamic reference has no referent.
// If there is no $dynamicRef, resolveDynamicRef returns nil, nil.
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2.
func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
if schema.DynamicRef == "" {
return nil, nil
}
// The ref behaves lexically or dynamically, but not both.
assert((schema.resolvedDynamicRef == nil) != (schema.dynamicRefAnchor == ""),
"DynamicRef not statically resolved properly")
if r := schema.resolvedDynamicRef; r != nil {
// Same as $ref.
return r, nil
}
// Dynamic behavior.
// Look for the base of the outermost schema on the stack with this dynamic
// anchor. (Yes, outermost: the one farthest from here. This the opposite
// of how ordinary dynamic variables behave.)
// Why the base of the schema being validated and not the schema itself?
// Because the base is the scope for anchors. In fact it's possible to
// refer to a schema that is not on the stack, but a child of some base
// on the stack.
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
for _, s := range st.stack {
info, ok := s.base.anchors[schema.dynamicRefAnchor]
if ok && info.dynamic {
return info.schema, nil
}
}
return nil, fmt.Errorf("missing dynamic anchor %q", schema.dynamicRefAnchor)
}
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
// a schema or sub-schema has a default, then a corresponding zero instance value
// is set to the default.
//
// The JSON Schema specification does not describe how defaults should be interpreted.
// This method honors defaults only on properties, and only those that are not required.
// If the instance is a map and the property is missing, the property is added to
// the map with the default.
// If the instance is a struct, the field corresponding to the property exists, and
// its value is zero, the field is set to the default.
// ApplyDefaults can panic if a default cannot be assigned to a field.
//
// The argument must be a pointer to the instance.
// (In case we decide that top-level defaults are meaningful.)
//
// It is recommended to first call Resolve with a ValidateDefaults option of true,
// then call this method, and lastly call Validate.
//
// TODO(jba): consider what defaults on top-level or array instances might mean.
// TODO(jba): follow $ref and $dynamicRef
// TODO(jba): apply defaults on sub-schemas to corresponding sub-instances.
func (rs *Resolved) ApplyDefaults(instancep any) error {
st := &state{rs: rs}
return st.applyDefaults(reflect.ValueOf(instancep), rs.root)
}
// Leave this as a potentially recursive helper function, because we'll surely want
// to apply defaults on sub-schemas someday.
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
defer wrapf(&err, "applyDefaults: schema %s, instance %v", schema, instancep)
instance := instancep.Elem()
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
if instance.Kind() == reflect.Map {
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
return fmt.Errorf("map key type %s is not a string", kt)
}
}
for prop, subschema := range schema.Properties {
// Ignore defaults on required properties. (A required property shouldn't have a default.)
if schema.isRequired[prop] {
continue
}
val := property(instance, prop)
switch instance.Kind() {
case reflect.Map:
// If there is a default for this property, and the map key is missing,
// set the map value to the default.
if subschema.Default != nil && !val.IsValid() {
// Create an lvalue, since map values aren't addressable.
lvalue := reflect.New(instance.Type().Elem())
if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil {
return err
}
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
}
case reflect.Struct:
// If there is a default for this property, and the field exists but is zero,
// set the field to the default.
if subschema.Default != nil && val.IsValid() && val.IsZero() {
if err := json.Unmarshal(subschema.Default, val.Addr().Interface()); err != nil {
return err
}
}
default:
panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s",
prop, instance, instance.Kind()))
}
}
}
return nil
}
// property returns the value of the property of v with the given name, or the invalid
// reflect.Value if there is none.
// If v is a map, the property is the value of the map whose key is name.
@ -548,6 +661,7 @@ func property(v reflect.Value, name string) reflect.Value {
return v.MapIndex(reflect.ValueOf(name))
case reflect.Struct:
props := structPropertiesOf(v.Type())
// Ignore nonexistent properties.
if index, ok := props[name]; ok {
return v.FieldByIndex(index)
}