json_schemer 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,7 @@ module JSONSchemer
35
35
  end
36
36
  end
37
37
 
38
- class ContentEncoding < Keyword
38
+ class ContentEncoding < Draft202012::Vocab::Content::ContentEncoding
39
39
  def error(formatted_instance_location:, **)
40
40
  "string at #{formatted_instance_location} could not be decoded using encoding: #{value}"
41
41
  end
@@ -43,13 +43,13 @@ module JSONSchemer
43
43
  def validate(instance, instance_location, keyword_location, _context)
44
44
  return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
45
45
 
46
- valid, annotation = Format.decode_content_encoding(instance, value)
46
+ valid, annotation = parsed.call(instance)
47
47
 
48
48
  result(instance, instance_location, keyword_location, valid, :annotation => annotation)
49
49
  end
50
50
  end
51
51
 
52
- class ContentMediaType < Keyword
52
+ class ContentMediaType < Draft202012::Vocab::Content::ContentMediaType
53
53
  def error(formatted_instance_location:, **)
54
54
  "string at #{formatted_instance_location} could not be parsed using media type: #{value}"
55
55
  end
@@ -58,7 +58,7 @@ module JSONSchemer
58
58
  return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
59
59
 
60
60
  decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance
61
- valid, annotation = Format.parse_content_media_type(decoded_instance, value)
61
+ valid, annotation = parsed.call(decoded_instance)
62
62
 
63
63
  result(instance, instance_location, keyword_location, valid, :annotation => annotation)
64
64
  end
@@ -1,15 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
3
  module Format
4
- include Duration
5
- include Email
6
- include Hostname
7
- include JSONPointer
8
- include URITemplate
4
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3
5
+ DATE_TIME = proc do |instance, _format|
6
+ !instance.is_a?(String) || valid_date_time?(instance)
7
+ end
8
+ DATE = proc do |instance, _format|
9
+ !instance.is_a?(String) || valid_date_time?("#{instance}T04:05:06.123456789+07:00")
10
+ end
11
+ TIME = proc do |instance, _format|
12
+ !instance.is_a?(String) || valid_date_time?("2001-02-03T#{instance}")
13
+ end
14
+ DURATION = proc do |instance, _format|
15
+ !instance.is_a?(String) || valid_duration?(instance)
16
+ end
17
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.2
18
+ EMAIL = proc do |instance, _format|
19
+ !instance.is_a?(String) || instance.ascii_only? && valid_email?(instance)
20
+ end
21
+ IDN_EMAIL = proc do |instance, _format|
22
+ !instance.is_a?(String) || valid_email?(instance)
23
+ end
24
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.3
25
+ HOSTNAME = proc do |instance, _format|
26
+ !instance.is_a?(String) || instance.ascii_only? && valid_hostname?(instance)
27
+ end
28
+ IDN_HOSTNAME = proc do |instance, _format|
29
+ !instance.is_a?(String) || valid_hostname?(instance)
30
+ end
31
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.4
32
+ IPV4 = proc do |instance, _format|
33
+ !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET)
34
+ end
35
+ IPV6 = proc do |instance, _format|
36
+ !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET6)
37
+ end
38
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.5
39
+ URI = proc do |instance, _format|
40
+ !instance.is_a?(String) || valid_uri?(instance)
41
+ end
42
+ URI_REFERENCE = proc do |instance, _format|
43
+ !instance.is_a?(String) || valid_uri_reference?(instance)
44
+ end
45
+ IRI = proc do |instance, _format|
46
+ !instance.is_a?(String) || valid_uri?(iri_escape(instance))
47
+ end
48
+ IRI_REFERENCE = proc do |instance, _format|
49
+ !instance.is_a?(String) || valid_uri_reference?(iri_escape(instance))
50
+ end
51
+ UUID = proc do |instance, _format|
52
+ !instance.is_a?(String) || valid_uuid?(instance)
53
+ end
54
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.6
55
+ URI_TEMPLATE = proc do |instance, _format|
56
+ !instance.is_a?(String) || valid_uri_template?(instance)
57
+ end
58
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.7
59
+ JSON_POINTER = proc do |instance, _format|
60
+ !instance.is_a?(String) || valid_json_pointer?(instance)
61
+ end
62
+ RELATIVE_JSON_POINTER = proc do |instance, _format|
63
+ !instance.is_a?(String) || valid_relative_json_pointer?(instance)
64
+ end
65
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.8
66
+ REGEX = proc do |instance, _format|
67
+ !instance.is_a?(String) || valid_regex?(instance)
68
+ end
9
69
 
10
70
  DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
11
- HOUR_24_REGEX = /T24/.freeze
12
- LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze
71
+ DATE_TIME_SEPARATOR_CHARACTER_CLASS = '[Tt\s]'
72
+ HOUR_24_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}24:/.freeze
73
+ LEAP_SECOND_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}\d{2}:\d{2}:6/.freeze
13
74
  IP_REGEX = /\A[\h:.]+\z/.freeze
14
75
  INVALID_QUERY_REGEX = /\s/.freeze
15
76
  IRI_ESCAPE_REGEX = /[^[:ascii:]]/
@@ -20,6 +81,12 @@ module JSONSchemer
20
81
  end.freeze
21
82
 
22
83
  class << self
84
+ include Duration
85
+ include Email
86
+ include Hostname
87
+ include JSONPointer
88
+ include URITemplate
89
+
23
90
  def percent_encode(data, regexp)
24
91
  data = data.dup
25
92
  data.force_encoding(Encoding::ASCII_8BIT)
@@ -27,126 +94,55 @@ module JSONSchemer
27
94
  data.force_encoding(Encoding::US_ASCII)
28
95
  end
29
96
 
30
- def decode_content_encoding(data, content_encoding)
31
- case content_encoding
32
- when 'base64'
33
- begin
34
- [true, Base64.strict_decode64(data)]
35
- rescue
36
- [false, nil]
37
- end
38
- else
39
- raise UnknownContentEncoding, content_encoding
40
- end
97
+ def valid_date_time?(data)
98
+ return false if HOUR_24_REGEX.match?(data)
99
+ datetime = DateTime.rfc3339(data)
100
+ return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
101
+ DATE_TIME_OFFSET_REGEX.match?(data)
102
+ rescue ArgumentError
103
+ false
41
104
  end
42
105
 
43
- def parse_content_media_type(data, content_media_type)
44
- case content_media_type
45
- when 'application/json'
46
- begin
47
- [true, JSON.parse(data)]
48
- rescue
49
- [false, nil]
50
- end
51
- else
52
- raise UnknownContentMediaType, content_media_type
53
- end
106
+ def valid_ip?(data, family)
107
+ IPAddr.new(data, family)
108
+ IP_REGEX.match?(data)
109
+ rescue IPAddr::Error
110
+ false
54
111
  end
55
- end
56
112
 
57
- def valid_spec_format?(data, format)
58
- case format
59
- when 'date-time'
60
- valid_date_time?(data)
61
- when 'date'
62
- valid_date_time?("#{data}T04:05:06.123456789+07:00")
63
- when 'time'
64
- valid_date_time?("2001-02-03T#{data}")
65
- when 'email'
66
- data.ascii_only? && valid_email?(data)
67
- when 'idn-email'
68
- valid_email?(data)
69
- when 'hostname'
70
- data.ascii_only? && valid_hostname?(data)
71
- when 'idn-hostname'
72
- valid_hostname?(data)
73
- when 'ipv4'
74
- valid_ip?(data, Socket::AF_INET)
75
- when 'ipv6'
76
- valid_ip?(data, Socket::AF_INET6)
77
- when 'uri'
78
- valid_uri?(data)
79
- when 'uri-reference'
80
- valid_uri_reference?(data)
81
- when 'iri'
82
- valid_uri?(iri_escape(data))
83
- when 'iri-reference'
84
- valid_uri_reference?(iri_escape(data))
85
- when 'uri-template'
86
- valid_uri_template?(data)
87
- when 'json-pointer'
88
- valid_json_pointer?(data)
89
- when 'relative-json-pointer'
90
- valid_relative_json_pointer?(data)
91
- when 'regex'
92
- valid_regex?(data)
93
- when 'duration'
94
- valid_duration?(data)
95
- when 'uuid'
96
- valid_uuid?(data)
97
- else
98
- raise UnknownFormat, format
113
+ def parse_uri_scheme(data)
114
+ scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data)
115
+ # ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
116
+ raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
117
+ scheme
99
118
  end
100
- end
101
-
102
- def valid_date_time?(data)
103
- return false if HOUR_24_REGEX.match?(data)
104
- datetime = DateTime.rfc3339(data)
105
- return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
106
- DATE_TIME_OFFSET_REGEX.match?(data)
107
- rescue ArgumentError
108
- false
109
- end
110
-
111
- def valid_ip?(data, family)
112
- IPAddr.new(data, family)
113
- IP_REGEX.match?(data)
114
- rescue IPAddr::Error
115
- false
116
- end
117
-
118
- def parse_uri_scheme(data)
119
- scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data)
120
- # URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
121
- raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
122
- scheme
123
- end
124
119
 
125
- def valid_uri?(data)
126
- !!parse_uri_scheme(data)
127
- rescue URI::InvalidURIError
128
- false
129
- end
120
+ def valid_uri?(data)
121
+ !!parse_uri_scheme(data)
122
+ rescue ::URI::InvalidURIError
123
+ false
124
+ end
130
125
 
131
- def valid_uri_reference?(data)
132
- parse_uri_scheme(data)
133
- true
134
- rescue URI::InvalidURIError
135
- false
136
- end
126
+ def valid_uri_reference?(data)
127
+ parse_uri_scheme(data)
128
+ true
129
+ rescue ::URI::InvalidURIError
130
+ false
131
+ end
137
132
 
138
- def iri_escape(data)
139
- Format.percent_encode(data, IRI_ESCAPE_REGEX)
140
- end
133
+ def iri_escape(data)
134
+ Format.percent_encode(data, IRI_ESCAPE_REGEX)
135
+ end
141
136
 
142
- def valid_regex?(data)
143
- !!EcmaRegexp.ruby_equivalent(data)
144
- rescue InvalidEcmaRegexp
145
- false
146
- end
137
+ def valid_regex?(data)
138
+ !!EcmaRegexp.ruby_equivalent(data)
139
+ rescue InvalidEcmaRegexp
140
+ false
141
+ end
147
142
 
148
- def valid_uuid?(data)
149
- UUID_REGEX.match?(data) || NIL_UUID == data
143
+ def valid_uuid?(data)
144
+ UUID_REGEX.match?(data) || NIL_UUID == data
145
+ end
150
146
  end
151
147
  end
152
148
  end
@@ -26,6 +26,10 @@ module JSONSchemer
26
26
  @schema_pointer ||= "#{parent.schema_pointer}/#{escaped_keyword}"
27
27
  end
28
28
 
29
+ def error_key
30
+ keyword
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def parse
@@ -2,6 +2,12 @@
2
2
  module JSONSchemer
3
3
  module OpenAPI30
4
4
  BASE_URI = URI('json-schemer://openapi30/schema')
5
+ # https://spec.openapis.org/oas/v3.0.3#data-types
6
+ FORMATS = OpenAPI31::FORMATS.merge(
7
+ 'byte' => proc { |instance, _value| ContentEncoding::BASE64.call(instance).first },
8
+ 'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT },
9
+ 'date' => Format::DATE
10
+ )
5
11
  SCHEMA = {
6
12
  'id' => 'json-schemer://openapi30/schema',
7
13
  '$schema' => 'http://json-schema.org/draft-04/schema#',
@@ -2,6 +2,14 @@
2
2
  module JSONSchemer
3
3
  module OpenAPI31
4
4
  BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base')
5
+ # https://spec.openapis.org/oas/v3.1.0#data-types
6
+ FORMATS = {
7
+ 'int32' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 32 },
8
+ 'int64' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 64 },
9
+ 'float' => proc { |instance, _format| instance.is_a?(Float) },
10
+ 'double' => proc { |instance, _format| instance.is_a?(Float) },
11
+ 'password' => proc { |_instance, _format| true }
12
+ }
5
13
  SCHEMA = {
6
14
  '$id' => 'https://spec.openapis.org/oas/3.1/dialect/base',
7
15
  '$schema' => 'https://json-schema.org/draft/2020-12/schema',
@@ -34,7 +34,8 @@ module JSONSchemer
34
34
  end
35
35
 
36
36
  class Discriminator < Keyword
37
- include Format::JSONPointer
37
+ # https://spec.openapis.org/oas/v3.1.0#components-object
38
+ FIXED_FIELD_REGEX = /\A[a-zA-Z0-9\.\-_]+$\z/
38
39
 
39
40
  attr_accessor :skip_ref_once
40
41
 
@@ -42,43 +43,80 @@ module JSONSchemer
42
43
  "value at #{formatted_instance_location} does not match `discriminator` schema"
43
44
  end
44
45
 
45
- def validate(instance, instance_location, keyword_location, context)
46
- property_name = value.fetch('propertyName')
47
- mapping = value['mapping'] || {}
46
+ def mapping
47
+ @mapping ||= value['mapping'] || {}
48
+ end
49
+
50
+ def subschemas_by_property_value
51
+ @subschemas_by_property_value ||= if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf')
52
+ subschemas = schema.parsed['anyOf']&.parsed || []
53
+ subschemas += schema.parsed['oneOf']&.parsed || []
48
54
 
49
- return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash) && instance.key?(property_name)
55
+ subschemas_by_ref = {}
56
+ subschemas_by_schema_name = {}
50
57
 
51
- property = instance.fetch(property_name)
52
- ref = mapping.fetch(property, property)
58
+ subschemas.each do |subschema|
59
+ subschema_ref = subschema.parsed.fetch('$ref').parsed
60
+ subschemas_by_ref[subschema_ref] = subschema
53
61
 
54
- ref_schema = nil
55
- unless ref.start_with?('#') && valid_json_pointer?(ref.delete_prefix('#'))
56
- ref_schema = begin
57
- root.resolve_ref(URI.join(schema.base_uri, "#/components/schemas/#{ref}"))
58
- rescue InvalidRefPointer
59
- nil
62
+ if subschema_ref.start_with?('#/components/schemas/')
63
+ schema_name = subschema_ref.delete_prefix('#/components/schemas/')
64
+ subschemas_by_schema_name[schema_name] = subschema if FIXED_FIELD_REGEX.match?(schema_name)
65
+ end
60
66
  end
61
- end
62
- ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref))
63
67
 
64
- return if skip_ref_once == ref_schema.absolute_keyword_location
68
+ explicit_mapping = mapping.transform_values do |schema_name_or_ref|
69
+ subschemas_by_schema_name.fetch(schema_name_or_ref) { subschemas_by_ref.fetch(schema_name_or_ref) }
70
+ end
65
71
 
66
- nested = []
72
+ implicit_mapping = subschemas_by_schema_name.reject do |_schema_name, subschema|
73
+ explicit_mapping.value?(subschema)
74
+ end
67
75
 
68
- if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf')
69
- subschemas = schema.parsed['anyOf']&.parsed || []
70
- subschemas += schema.parsed['oneOf']&.parsed || []
71
- subschemas.each do |subschema|
72
- if subschema.parsed.fetch('$ref').ref_schema.absolute_keyword_location == ref_schema.absolute_keyword_location
73
- nested << subschema.validate_instance(instance, instance_location, keyword_location, context)
76
+ implicit_mapping.merge(explicit_mapping)
77
+ else
78
+ Hash.new do |hash, property_value|
79
+ schema_name_or_ref = mapping.fetch(property_value, property_value)
80
+
81
+ subschema = nil
82
+
83
+ if FIXED_FIELD_REGEX.match?(schema_name_or_ref)
84
+ subschema = begin
85
+ schema.ref("#/components/schemas/#{schema_name_or_ref}")
86
+ rescue InvalidRefPointer
87
+ nil
88
+ end
89
+ end
90
+
91
+ subschema ||= begin
92
+ schema.ref(schema_name_or_ref)
93
+ rescue InvalidRefResolution, UnknownRef
94
+ nil
74
95
  end
96
+
97
+ hash[property_value] = subschema
75
98
  end
76
- else
77
- ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
78
- nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context)
79
99
  end
100
+ end
101
+
102
+ def validate(instance, instance_location, keyword_location, context)
103
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
104
+
105
+ property_name = value.fetch('propertyName')
106
+
107
+ return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name)
108
+
109
+ property_value = instance.fetch(property_name)
110
+ subschema = subschemas_by_property_value[property_value]
111
+
112
+ return result(instance, instance_location, keyword_location, false) unless subschema
113
+
114
+ return if skip_ref_once == subschema.absolute_keyword_location
115
+ subschema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
116
+
117
+ subschema_result = subschema.validate_instance(instance, instance_location, keyword_location, context)
80
118
 
81
- result(instance, instance_location, keyword_location, (nested.any? && nested.all?(&:valid)), nested)
119
+ result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
82
120
  ensure
83
121
  self.skip_ref_once = nil
84
122
  end
@@ -5,14 +5,15 @@ module JSONSchemer
5
5
 
6
6
  attr_reader :keyword, :schema
7
7
 
8
+ def x_error
9
+ return @x_error if defined?(@x_error)
10
+ @x_error = schema.parsed['x-error']&.message(error_key)
11
+ end
12
+
8
13
  private
9
14
 
10
15
  def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false)
11
- if valid
12
- Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations')
13
- else
14
- Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors')
15
- end
16
+ Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, valid ? 'annotations' : 'errors')
16
17
  end
17
18
 
18
19
  def escaped_keyword
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
- Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
4
- CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
5
- hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
6
- end
3
+ CATCHALL = '*'
4
+ I18N_SEPARATOR = "\x1F" # unit separator
5
+ I18N_SCOPE = 'json_schemer'
6
+ I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors"
7
+ X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/
8
+ CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
9
+ hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
10
+ end
7
11
 
12
+ Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
8
13
  def output(output_format)
9
14
  case output_format
10
15
  when 'classic'
@@ -24,10 +29,59 @@ module JSONSchemer
24
29
 
25
30
  def error
26
31
  return @error if defined?(@error)
27
- resolved_instance_location = Location.resolve(instance_location)
28
- @error = source.error(
29
- :formatted_instance_location => resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`",
30
- :details => details
32
+ if source.x_error
33
+ # not using sprintf because it warns: "too many arguments for format string"
34
+ @error = source.x_error.gsub(
35
+ X_ERROR_REGEX,
36
+ '%{instance}' => instance,
37
+ '%{instanceLocation}' => Location.resolve(instance_location),
38
+ '%{keywordLocation}' => Location.resolve(keyword_location),
39
+ '%{absoluteKeywordLocation}' => source.absolute_keyword_location
40
+ )
41
+ @x_error = true
42
+ else
43
+ resolved_instance_location = Location.resolve(instance_location)
44
+ formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`"
45
+ @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details)
46
+ if i18n?
47
+ begin
48
+ @error = i18n!
49
+ @i18n = true
50
+ rescue I18n::MissingTranslationData
51
+ end
52
+ end
53
+ end
54
+ @error
55
+ end
56
+
57
+ def i18n?
58
+ return @@i18n if defined?(@@i18n)
59
+ @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE)
60
+ end
61
+
62
+ def i18n!
63
+ base_uri_str = source.schema.base_uri.to_s
64
+ meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s
65
+ resolved_keyword_location = Location.resolve(keyword_location)
66
+ error_key = source.error_key
67
+ I18n.translate!(
68
+ source.absolute_keyword_location,
69
+ :default => [
70
+ "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}",
71
+ "##{resolved_keyword_location}",
72
+ "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}",
73
+ "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
74
+ "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}",
75
+ "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
76
+ error_key,
77
+ CATCHALL
78
+ ].map!(&:to_sym),
79
+ :separator => I18N_SEPARATOR,
80
+ :scope => I18N_ERRORS_SCOPE,
81
+ :instance => instance,
82
+ :instanceLocation => Location.resolve(instance_location),
83
+ :keywordLocation => resolved_keyword_location,
84
+ :absoluteKeywordLocation => source.absolute_keyword_location
31
85
  )
32
86
  end
33
87
 
@@ -38,8 +92,13 @@ module JSONSchemer
38
92
  'absoluteKeywordLocation' => source.absolute_keyword_location,
39
93
  'instanceLocation' => Location.resolve(instance_location)
40
94
  }
41
- out['error'] = error unless valid
42
- out['annotation'] = annotation if valid && annotation
95
+ if valid
96
+ out['annotation'] = annotation if annotation
97
+ else
98
+ out['error'] = error
99
+ out['x-error'] = true if @x_error
100
+ out['i18n'] = true if @i18n
101
+ end
43
102
  out
44
103
  end
45
104
 
@@ -54,6 +113,8 @@ module JSONSchemer
54
113
  'type' => type || CLASSIC_ERROR_TYPES[source.class]
55
114
  }
56
115
  out['error'] = error
116
+ out['x-error'] = true if @x_error
117
+ out['i18n'] = true if @i18n
57
118
  out['details'] = details if details
58
119
  out
59
120
  end