Class: Cocina::Models::Validators::JsonSchemaValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/cocina/models/validators/json_schema_validator.rb

Overview

Validates Cocina model instances against the JSON schema.

The schema uses OpenAPI 3.1.0 conventions with unevaluatedProperties: false to enforce strict validation. However, allOf/$ref composition causes two categories of cascade noise in the structured evaluation output that must be filtered before reporting:

1. falseSchema entries — every `unevaluatedProperties: false` sub-schema emits a
 "False schema does not allow …" entry for each value at that path. These are always
 redundant with the parent unevaluatedProperties entry.

2. Root-level unevaluatedProperties listing only known model attributes as unexpected —
 the validator can't prove top-level fields were "evaluated" through allOf/$ref, so it
 reports every legitimate root property as unexpected at root "".

For example, validating a DRO with an unexpected releaseTags in administrative produces one actionable error plus ~12 cascade entries. The de-noiser keeps the actionable error and discards the rest.

Constant Summary collapse

SCHEMA_PATH =
::File.expand_path('../../../../schema.json', __dir__)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(clazz, attributes) ⇒ JsonSchemaValidator

Returns a new instance of JsonSchemaValidator.

Parameters:

  • clazz (Class)

    the Cocina model class being validated (e.g., Cocina::Models::DRO)

  • attributes (Hash)

    the attributes of the model instance being validated



53
54
55
56
# File 'lib/cocina/models/validators/json_schema_validator.rb', line 53

def initialize(clazz, attributes)
  @clazz = clazz
  @attributes = attributes
end

Class Method Details

.documentHash

Returns a hash representation of the schema.json document.

Returns:

  • (Hash)

    a hash representation of the schema.json document



44
45
46
47
48
49
# File 'lib/cocina/models/validators/json_schema_validator.rb', line 44

def self.document
  @document ||= begin
    file_content = ::File.read(SCHEMA_PATH)
    JSON.parse(file_content)
  end
end

.validateObject

See Also:



27
28
29
30
31
32
33
34
# File 'lib/cocina/models/validators/json_schema_validator.rb', line 27

def self.validate(...)
  # Skip when nested inside another Validatable's construction: the enclosing object's own
  # schema check already covers this, so re-evaluating it here would be
  # redundant. See Validatable#new.
  return if Thread.current[:cocina_construction_depth].to_i.positive?

  new(...).validate
end

.validator_for(def_name) ⇒ JSONSchema validator

Returns a cached per-definition validator.

Returns:

  • (JSONSchema validator)

    a cached per-definition validator



37
38
39
40
41
# File 'lib/cocina/models/validators/json_schema_validator.rb', line 37

def self.validator_for(def_name)
  @validators ||= {}
  schema = { '$ref' => "#/$defs/#{def_name}", '$defs' => document['$defs'] }
  @validators[def_name] ||= JSONSchema.validator_for(schema, validate_formats: true)
end

Instance Method Details

#validateNilClass

Validates attributes against the Cocina model schema.

Injects the cocinaVersion if the model includes it as an attribute, then validates the attributes against the schema definition for this model. De-noises unevaluatedProperties cascade errors before raising a ValidationError.

Uses the structured evaluation API to capture sub-errors inside anyOf branches, producing actionable messages (e.g. path-qualified minItems/pattern failures) rather than the generic "not valid under anyOf" message.

Returns:

  • (NilClass)

    returns nil if validation passes

Raises:

  • (ValidationError)

    if validation fails, with a de-noised error message



70
71
72
73
74
75
76
77
# File 'lib/cocina/models/validators/json_schema_validator.rb', line 70

def validate
  attributes['cocinaVersion'] = Cocina::Models::VERSION if clazz.attribute_names.include?(:cocinaVersion)

  evaluation = self.class.validator_for(method_name).evaluate(attributes.as_json)
  return if evaluation.valid?

  raise ValidationError, "When validating #{method_name}: " + filtered_error_messages(evaluation).join('; ')
end