diff --git a/schema/config-schema.json b/schema/config-schema.json index cf32d07..e86398b 100644 --- a/schema/config-schema.json +++ b/schema/config-schema.json @@ -42,8 +42,7 @@ "capabilities": { "$ref": "#/$defs/ModelCapabilities" } - }, - "additionalProperties": false + } }, "ModelDescriptor": { "type": "object", @@ -92,8 +91,7 @@ "description": { "type": "string" } - }, - "additionalProperties": false + } }, "ModelFS": { "type": "object", @@ -110,7 +108,6 @@ "minItems": 1 } }, - "additionalProperties": false, "required": [ "type", "diffIds" diff --git a/schema/example_test.go b/schema/example_test.go index a91787a..c292f95 100644 --- a/schema/example_test.go +++ b/schema/example_test.go @@ -61,7 +61,7 @@ func validate(t *testing.T, name string) { continue } - err = schema.Validator(example.Mediatype).Validate(strings.NewReader(example.Body)) + err = schema.Validator(example.Mediatype).ValidateNoUnknownFields(strings.NewReader(example.Body)) if err == nil { printFields(t, "ok", example.Mediatype, example.Title) } else { diff --git a/schema/validator.go b/schema/validator.go index cc3c79b..1b80231 100644 --- a/schema/validator.go +++ b/schema/validator.go @@ -26,33 +26,55 @@ import ( "github.com/santhosh-tekuri/jsonschema/v5" ) -// Validator wraps a media type string identifier and implements validation against a JSON schema. +// Validate validates the given reader against the schema of the wrapped media type. +// By default, unknown fields are allowed unless `additionalProperties` is explicitly set to `false` +// correspondingly in the json schema. type Validator string -// Validate validates the given reader against the schema of the wrapped media type. -func (v Validator) Validate(src io.Reader) error { +func (v Validator) validateByMediaType(src io.Reader) (io.Reader, error) { // run the media type specific validation if fn, ok := validateByMediaType[v]; ok { if fn == nil { - return fmt.Errorf("internal error: mapValidate is nil for %s", string(v)) + return nil, fmt.Errorf("internal error: mapValidate is nil for %s", string(v)) } // buffer the src so the media type validation and the schema validation can both read it buf, err := io.ReadAll(src) if err != nil { - return fmt.Errorf("failed to read input: %w", err) + return nil, fmt.Errorf("failed to read input: %w", err) } src = bytes.NewReader(buf) err = fn(buf) if err != nil { - return err + return nil, err } } + return src, nil +} + +// Validate validates the given reader against the schema of the wrapped media type. +func (v Validator) Validate(src io.Reader) error { + srcReader, err := v.validateByMediaType(src) + if err != nil { + return err + } + // json schema validation - return v.validateSchema(src) + return v.validateSchema(srcReader, false) +} + +// ValidateNoUnknownFields validates the given reader against the schema of the wrapped media type and +// rejects if there are any unknown fields. +func (v Validator) ValidateNoUnknownFields(src io.Reader) error { + srcReader, err := v.validateByMediaType(src) + if err != nil { + return err + } + + return v.validateSchema(srcReader, true) } -func (v Validator) validateSchema(src io.Reader) error { +func (v Validator) validateSchema(src io.Reader, rejectUnknownfields bool) error { if _, ok := specs[v]; !ok { return fmt.Errorf("no validator available for %s", string(v)) } @@ -94,6 +116,10 @@ func (v Validator) validateSchema(src io.Reader) error { return fmt.Errorf("failed to compile schema %s: %w", string(v), err) } + if rejectUnknownfields { + forceSetAdditionalPropertiesFalse(schema) + } + // read in the user input and validate var input interface{} err = json.NewDecoder(src).Decode(&input) @@ -123,3 +149,38 @@ func validateConfig(buf []byte) error { return nil } + +// forceSetAdditionalPropertiesFalse recursively traverses the given JSON schema +// and sets the `AdditionalProperties` field to `false` for all schema objects +// encountered. This ensures that validation will reject any properties not explicitly +// defined in the schema. +// +// This function modifies the schema in place. +func forceSetAdditionalPropertiesFalse(schema *jsonschema.Schema) { + if len(schema.Types) == 0 { + return + } + + // We don't have any cases where multiple types are defined for a single field. + t := schema.Types[0] + if t == "object" { + schema.AdditionalProperties = false + } + + // Recurse into properties + if schema.Properties != nil { + for _, propSchema := range schema.Properties { + forceSetAdditionalPropertiesFalse(propSchema) + } + } + + // Recurse into items (for arrays) + if schema.Items != nil { + forceSetAdditionalPropertiesFalse(schema.Items.(*jsonschema.Schema)) + } + + // Recurse into additionalProperties if it's a schema + if s, ok := schema.AdditionalProperties.(*jsonschema.Schema); ok { + forceSetAdditionalPropertiesFalse(s) + } +}