# This file defines a custom validator class `JsonSchemaValidator` for validating a JSON object against a schema. # To use this validator, define a schema as a Ruby hash and include it in the validation options when validating a model. # The schema should define the expected structure and types of the JSON object, as well as any validation rules. # Here's an example schema: # # schema = { # 'type' => 'object', # 'properties' => { # 'name' => { 'type' => 'string' }, # 'age' => { 'type' => 'integer' }, # 'is_active' => { 'type' => 'boolean' }, # 'tags' => { 'type' => 'array' }, # 'address' => { # 'type' => 'object', # 'properties' => { # 'street' => { 'type' => 'string' }, # 'city' => { 'type' => 'string' } # }, # 'required' => ['street', 'city'] # } # }, # 'required': ['name', 'age'] # }.to_json.freeze # # To validate a model using this schema, include the `JsonSchemaValidator` in the model's validations and pass the schema # as an option: # # class MyModel < ApplicationRecord # validates_with JsonSchemaValidator, schema: schema # end class JsonSchemaValidator < ActiveModel::Validator def validate(record) # Get the attribute resolver function from options or use a default one attribute_resolver = options[:attribute_resolver] || ->(rec) { rec.additional_attributes } # Resolve the JSON data to be validated json_data = attribute_resolver.call(record) # Get the schema to be used for validation schema = options[:schema] # Create a JSONSchemer instance using the schema schemer = JSONSchemer.schema(schema) # Validate the JSON data against the schema validation_errors = schemer.validate(json_data) # Add validation errors to the record with a formatted statement validation_errors.each do |error| format_and_append_error(error, record) end end private def format_and_append_error(error, record) return handle_required(error, record) if error['type'] == 'required' return handle_minimum(error, record) if error['type'] == 'minimum' return handle_maximum(error, record) if error['type'] == 'maximum' type = error['type'] == 'object' ? 'hash' : error['type'] handle_type(error, record, type) end def handle_required(error, record) missing_values = error['details']['missing_keys'] missing_values.each do |missing| record.errors.add(missing, 'is required') end end def handle_type(error, record, expected_type) data = get_name_from_data_pointer(error) record.errors.add(data, "must be of type #{expected_type}") end def handle_minimum(error, record) data = get_name_from_data_pointer(error) record.errors.add(data, "must be greater than or equal to #{error['schema']['minimum']}") end def handle_maximum(error, record) data = get_name_from_data_pointer(error) record.errors.add(data, "must be less than or equal to #{error['schema']['maximum']}") end def get_name_from_data_pointer(error) data = error['data_pointer'] # if data starts with a "/" remove it data[1..] if data[0] == '/' end end