module JTD

JTD is an implementation of JSON Type Definition validation for Ruby.

Constants

VERSION

The version of the jtd gem you are using.

Public Class Methods

validate(schema, instance, options = ValidationOptions.new) click to toggle source

Validates instance against schema according to the JSON Type Definition specification.

Returns a list of ValidationError. If there are no validation errors, then the returned list will be empty.

By default, all errors are returned, and an unlimited number of references will be followed. If you are running validate against schemas that may return a lot of errors, or which may contain circular references, then this can cause performance issues or stack overflows.

To mitigate this risk, consider using options, which must be an instance of ValidationOptions, to limit the number of errors returned or references followed.

If ValidationOptions#max_depth is reached, then validate will raise a MaxDepthExceededError.

The return value of validate is not well-defined if the schema is not valid, i.e. Schema#verify raises an error.

# File lib/jtd/validate.rb, line 24
def self.validate(schema, instance, options = ValidationOptions.new)
  state = ValidationState.new
  state.options = options
  state.root_schema = schema
  state.instance_tokens = []
  state.schema_tokens = [[]]
  state.errors = []

  begin
    validate_with_state(state, schema, instance)
  rescue MaxErrorsReachedError
    # This is just a dummy error to immediately stop validation. We swallow
    # the error here, and return the abridged set of errors.
  end

  state.errors
end

Private Class Methods

validate_int(state, instance, min, max) click to toggle source
# File lib/jtd/validate.rb, line 354
def self.validate_int(state, instance, min, max)
  if instance.is_a?(Numeric)
    if instance.modulo(1).nonzero? || instance < min || instance > max
      state.push_error
    end
  else
    state.push_error
  end
end
validate_with_state(state, schema, instance, parent_tag = nil) click to toggle source
# File lib/jtd/validate.rb, line 152
def self.validate_with_state(state, schema, instance, parent_tag = nil)
  return if schema.nullable && instance.nil?

  case schema.form
  when :ref
    state.schema_tokens << ['definitions', schema.ref]
    raise MaxDepthExceededError.new if state.schema_tokens.length == state.options.max_depth

    validate_with_state(state, state.root_schema.definitions[schema.ref], instance)
    state.schema_tokens.pop

  when :type
    state.push_schema_token('type')

    case schema.type
    when 'boolean'
      state.push_error unless instance == true || instance == false
    when 'float32', 'float64'
      state.push_error unless instance.is_a?(Numeric)
    when 'int8'
      validate_int(state, instance, -128, 127)
    when 'uint8'
      validate_int(state, instance, 0, 255)
    when 'int16'
      validate_int(state, instance, -32_768, 32_767)
    when 'uint16'
      validate_int(state, instance, 0, 65_535)
    when 'int32'
      validate_int(state, instance, -2_147_483_648, 2_147_483_647)
    when 'uint32'
      validate_int(state, instance, 0, 4_294_967_295)
    when 'string'
      state.push_error unless instance.is_a?(String)
    when 'timestamp'
      begin
        DateTime.rfc3339(instance)
      rescue TypeError, ArgumentError
        state.push_error
      end
    end

    state.pop_schema_token

  when :enum
    state.push_schema_token('enum')
    state.push_error unless schema.enum.include?(instance)
    state.pop_schema_token

  when :elements
    state.push_schema_token('elements')

    if instance.is_a?(Array)
      instance.each_with_index do |sub_instance, index|
        state.push_instance_token(index.to_s)
        validate_with_state(state, schema.elements, sub_instance)
        state.pop_instance_token
      end
    else
      state.push_error
    end

    state.pop_schema_token

  when :properties
    # The properties form is a little weird. The JSON Typedef spec always
    # works out so that the schema path points to a part of the schema that
    # really exists, and there's no guarantee that a schema of the properties
    # form has the properties keyword.
    #
    # To deal with this, we handle the "instance isn't even an object" case
    # separately.
    unless instance.is_a?(Hash)
      if schema.properties
        state.push_schema_token('properties')
      else
        state.push_schema_token('optionalProperties')
      end

      state.push_error
      state.pop_schema_token

      return
    end

    # Check the required properties.
    if schema.properties
      state.push_schema_token('properties')

      schema.properties.each do |property, sub_schema|
        state.push_schema_token(property)

        if instance.key?(property)
          state.push_instance_token(property)
          validate_with_state(state, sub_schema, instance[property])
          state.pop_instance_token
        else
          state.push_error
        end

        state.pop_schema_token
      end

      state.pop_schema_token
    end

    # Check the optional properties. This is almost identical to the previous
    # case, except we don't raise an error if the property isn't present on
    # the instance.
    if schema.optional_properties
      state.push_schema_token('optionalProperties')

      schema.optional_properties.each do |property, sub_schema|
        state.push_schema_token(property)

        if instance.key?(property)
          state.push_instance_token(property)
          validate_with_state(state, sub_schema, instance[property])
          state.pop_instance_token
        end

        state.pop_schema_token
      end

      state.pop_schema_token
    end

    # Check for unallowed additional properties.
    unless schema.additional_properties
      properties = (schema.properties || {}).keys
      optional_properties = (schema.optional_properties || {}).keys
      parent_tags = [parent_tag]

      additional_keys = instance.keys - properties - optional_properties - parent_tags
      additional_keys.each do |property|
        state.push_instance_token(property)
        state.push_error
        state.pop_instance_token
      end
    end

  when :values
    state.push_schema_token('values')

    if instance.is_a?(Hash)
      instance.each do |property, sub_instance|
        state.push_instance_token(property)
        validate_with_state(state, schema.values, sub_instance)
        state.pop_instance_token
      end
    else
      state.push_error
    end

    state.pop_schema_token

  when :discriminator
    unless instance.is_a?(Hash)
      state.push_schema_token('discriminator')
      state.push_error
      state.pop_schema_token

      return
    end

    unless instance.key?(schema.discriminator)
      state.push_schema_token('discriminator')
      state.push_error
      state.pop_schema_token

      return
    end

    unless instance[schema.discriminator].is_a?(String)
      state.push_schema_token('discriminator')
      state.push_instance_token(schema.discriminator)
      state.push_error
      state.pop_instance_token
      state.pop_schema_token

      return
    end

    unless schema.mapping.key?(instance[schema.discriminator])
      state.push_schema_token('mapping')
      state.push_instance_token(schema.discriminator)
      state.push_error
      state.pop_instance_token
      state.pop_schema_token

      return
    end

    sub_schema = schema.mapping[instance[schema.discriminator]]

    state.push_schema_token('mapping')
    state.push_schema_token(instance[schema.discriminator])
    validate_with_state(state, sub_schema, instance, schema.discriminator)
    state.pop_schema_token
    state.pop_schema_token
  end
end