Files
query-orchestration/internal/customschema/validator.go
T
Jay Brown 17fc813823 Merged in feature/mutable-metadata1 (pull request #221)
M1, M2 and M3 complete

* M1, M2 and M3 complete

* review changes

* docs

* docs
2026-04-16 23:11:26 +00:00

129 lines
4.8 KiB
Go

package customschema
import (
"bytes"
"encoding/json"
"fmt"
"github.com/santhosh-tekuri/jsonschema/v6"
)
// SchemaValidator validates JSON Schema definitions and metadata payloads
// against the rules defined in Plan Section 6.2 / Tracking Section 1.2.
type SchemaValidator struct{}
// ValidateSchemaDefinition enforces the mutable-metadata schema rules on
// an incoming JSON Schema document. The rules are applied in this order
// so each failure mode surfaces a specific, greppable error substring:
// 1. Size ceiling (strict greater-than MaxSchemaDefinitionBytes).
// 2. JSON well-formedness.
// 3. JSON Schema draft 2020-12 meta-validation.
// 4. Root "type" must equal "object".
// 5. Root "additionalProperties" key must be present (true or false).
//
// Returns nil when every rule passes.
func (v *SchemaValidator) ValidateSchemaDefinition(schemaDef json.RawMessage) error {
// Rule 1: size ceiling. Strict greater-than — a payload whose length
// is exactly MaxSchemaDefinitionBytes must still be accepted.
if len(schemaDef) > MaxSchemaDefinitionBytes {
return fmt.Errorf(
"schema definition exceeds maximum size of %d bytes",
MaxSchemaDefinitionBytes,
)
}
// Rule 2: JSON well-formedness. Decode into a generic map so we can
// inspect the root keys after meta-validation.
var root map[string]any
if err := json.Unmarshal(schemaDef, &root); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
// Rule 3: JSON Schema draft 2020-12 meta-validation. Use the
// santhosh-tekuri compiler, which validates the supplied schema
// against the 2020-12 meta-schema during Compile().
compiler := jsonschema.NewCompiler()
parsed, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaDef))
if err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
if err := compiler.AddResource("schema.json", parsed); err != nil {
return fmt.Errorf("schema does not compile as a valid JSON Schema (type check failed): %w", err)
}
if _, err := compiler.Compile("schema.json"); err != nil {
return fmt.Errorf("schema does not compile as a valid JSON Schema (type check failed): %w", err)
}
// Rule 4: root "type" must equal "object". This check runs after
// meta-validation so a clearly-malformed schema surfaces a type
// error from the library rather than our generic "object" message.
rawType, hasType := root["type"]
if !hasType {
return fmt.Errorf(`root schema must declare type "object" (type key missing)`)
}
typeStr, ok := rawType.(string)
if !ok || typeStr != "object" {
return fmt.Errorf(`root schema type must be "object"`)
}
// Rule 5: root "additionalProperties" must be present as a boolean.
// Only true or false are accepted — a sub-schema object is not.
// The feature's strict/open switch is a binary choice per plan Section 6.2.
apVal, present := root["additionalProperties"]
if !present {
return fmt.Errorf(
"root schema must explicitly declare additionalProperties (true or false)",
)
}
if _, isBool := apVal.(bool); !isBool {
return fmt.Errorf(
"root schema must explicitly declare additionalProperties (true or false)",
)
}
return nil
}
// ValidateMetadata validates a metadata payload against a schema definition.
// Both arguments must be well-formed JSON. The schema definition is assumed
// to have already passed ValidateSchemaDefinition — ValidateMetadata only
// runs the instance check, not the meta-validation pass.
//
// Returns nil when the payload conforms to the schema. Returns a wrapped
// error whose message contains "invalid JSON" for malformed inputs or the
// underlying jsonschema validation error for structural mismatches.
func (v *SchemaValidator) ValidateMetadata(metadata json.RawMessage, schemaDef json.RawMessage) error {
// Reject empty payloads before touching the compiler. An empty body is
// never a valid JSON Schema instance — the test expects a clear error.
if len(metadata) == 0 {
return fmt.Errorf("invalid JSON: metadata payload is empty")
}
// Compile the schema. The schema is re-parsed on every call because
// the library's compiler holds references to the parsed resource.
compiler := jsonschema.NewCompiler()
parsedSchema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaDef))
if err != nil {
return fmt.Errorf("invalid schema definition: %w", err)
}
if err := compiler.AddResource("schema.json", parsedSchema); err != nil {
return fmt.Errorf("invalid schema definition: %w", err)
}
compiled, err := compiler.Compile("schema.json")
if err != nil {
return fmt.Errorf("invalid schema definition: %w", err)
}
// Parse the metadata payload.
parsedMetadata, err := jsonschema.UnmarshalJSON(bytes.NewReader(metadata))
if err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
// Run instance validation.
if err := compiled.Validate(parsedMetadata); err != nil {
return fmt.Errorf("metadata does not conform to schema: %w", err)
}
return nil
}