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 }