Skip to content
Closed
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
7 changes: 2 additions & 5 deletions schema/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@
"capabilities": {
"$ref": "#/$defs/ModelCapabilities"
}
},
"additionalProperties": false
}
},
"ModelDescriptor": {
"type": "object",
Expand Down Expand Up @@ -92,8 +91,7 @@
"description": {
"type": "string"
}
},
"additionalProperties": false
}
},
"ModelFS": {
"type": "object",
Expand All @@ -110,7 +108,6 @@
"minItems": 1
}
},
"additionalProperties": false,
"required": [
"type",
"diffIds"
Expand Down
2 changes: 1 addition & 1 deletion schema/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
77 changes: 69 additions & 8 deletions schema/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist please update the doc of Validate given that ValidateNoUnknownFields is added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right, caozhuozi! With the introduction of ValidateNoUnknownFields, it's crucial to clarify the behavior of the original Validate method in its documentation. This helps consumers of the API understand the distinction between the two validation approaches regarding unknown fields.

Since Validate calls validateSchema with rejectUnknownfields set to false, it implies that Validate allows unknown fields by default. I recommend updating the docstring for Validate (line 53) to reflect this:

// Validate validates the given reader against the schema of the wrapped media type. By default, it allows unknown fields.

This makes the API contract clearer and prevents potential confusion.

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))
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Loading