Skip to content

Commit 03fdc8e

Browse files
committed
Better error messages
This adds more descriptive error messages that are defined in keyword classes. It also differentiates "type" (classic output format) and "error" to make things a little more clear. The new "error" key in the classic output format is a slight behavior change, but it seems unlikely to break things. I don't love the "instance at `/whatever`" and "instance at root" wording, but I had trouble coming up with something generic. Hopefully these can be improved in the future.
1 parent 4f3c025 commit 03fdc8e

File tree

18 files changed

+327
-73
lines changed

18 files changed

+327
-73
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ schemer.validate({ 'abc' => 10 }).to_a
5050
# "schema"=>{"type"=>"integer", "minimum"=>11},
5151
# "schema_pointer"=>"/properties/abc",
5252
# "root_schema"=>{"type"=>"object", "properties"=>{"abc"=>{"type"=>"integer", "minimum"=>11}}},
53-
# "type"=>"minimum"}]
53+
# "type"=>"minimum",
54+
# "error"=>"number at `/abc` is less than: 11"}]
5455

5556
# default property values
5657

@@ -91,7 +92,8 @@ JSONSchemer.validate_schema({ '$id' => '#invalid' }).to_a
9192
# "schema"=>{"$ref"=>"#/$defs/uriReferenceString", "$comment"=>"Non-empty fragments not allowed.", "pattern"=>"^[^#]*#?$"},
9293
# "schema_pointer"=>"/properties/$id",
9394
# "root_schema"=>{...meta schema},
94-
# "type"=>"pattern"}]
95+
# "type"=>"pattern",
96+
# "error"=>"string at `/$id` does not match pattern: ^[^#]*#?$"}]
9597

9698
JSONSchemer.schema({ '$id' => 'valid' }).valid_schema?
9799
# => true
@@ -102,7 +104,8 @@ JSONSchemer.schema({ '$id' => '#invalid' }).validate_schema.to_a
102104
# "schema"=>{"$ref"=>"#/$defs/uriReferenceString", "$comment"=>"Non-empty fragments not allowed.", "pattern"=>"^[^#]*#?$"},
103105
# "schema_pointer"=>"/properties/$id",
104106
# "root_schema"=>{...meta schema},
105-
# "type"=>"pattern"}]
107+
# "type"=>"pattern",
108+
# "error"=>"string at `/$id` does not match pattern: ^[^#]*#?$"}]
106109
```
107110

108111
## Options

lib/json_schemer/draft201909/vocab/applicator.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module Draft201909
44
module Vocab
55
module Applicator
66
class Items < Keyword
7+
def error(formatted_instance_location:, **)
8+
"array items at #{formatted_instance_location} do not match `items` schema(s)"
9+
end
10+
711
def parse
812
if value.is_a?(Array)
913
value.map.with_index do |subschema, index|
@@ -32,6 +36,10 @@ def validate(instance, instance_location, keyword_location, context)
3236
end
3337

3438
class AdditionalItems < Keyword
39+
def error(formatted_instance_location:, **)
40+
"array items at #{formatted_instance_location} do not match `additionalItems` schema"
41+
end
42+
3543
def parse
3644
subschema(value)
3745
end
@@ -51,6 +59,10 @@ def validate(instance, instance_location, keyword_location, context)
5159
end
5260

5361
class UnevaluatedItems < Keyword
62+
def error(formatted_instance_location:, **)
63+
"array items at #{formatted_instance_location} do not match `unevaluatedItems` schema"
64+
end
65+
5466
def parse
5567
subschema(value)
5668
end

lib/json_schemer/draft202012/vocab/applicator.rb

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module Draft202012
44
module Vocab
55
module Applicator
66
class AllOf < Keyword
7+
def error(formatted_instance_location:, **)
8+
"value at #{formatted_instance_location} does not match all `allOf` schemas"
9+
end
10+
711
def parse
812
value.map.with_index do |subschema, index|
913
subschema(subschema, index.to_s)
@@ -19,6 +23,10 @@ def validate(instance, instance_location, keyword_location, context)
1923
end
2024

2125
class AnyOf < Keyword
26+
def error(formatted_instance_location:, **)
27+
"value at #{formatted_instance_location} does not match any `anyOf` schemas"
28+
end
29+
2230
def parse
2331
value.map.with_index do |subschema, index|
2432
subschema(subschema, index.to_s)
@@ -34,6 +42,10 @@ def validate(instance, instance_location, keyword_location, context)
3442
end
3543

3644
class OneOf < Keyword
45+
def error(formatted_instance_location:, **)
46+
"value at #{formatted_instance_location} does not match exactly one `oneOf` schema"
47+
end
48+
3749
def parse
3850
value.map.with_index do |subschema, index|
3951
subschema(subschema, index.to_s)
@@ -50,6 +62,10 @@ def validate(instance, instance_location, keyword_location, context)
5062
end
5163

5264
class Not < Keyword
65+
def error(formatted_instance_location:, **)
66+
"value at #{formatted_instance_location} matches `not` schema"
67+
end
68+
5369
def parse
5470
subschema(value)
5571
end
@@ -72,28 +88,42 @@ def validate(instance, instance_location, keyword_location, context)
7288
end
7389

7490
class Then < Keyword
91+
def error(formatted_instance_location:, **)
92+
"value at #{formatted_instance_location} does not match conditional `then` schema"
93+
end
94+
7595
def parse
7696
subschema(value)
7797
end
7898

7999
def validate(instance, instance_location, keyword_location, context)
80100
return unless context.adjacent_results.key?(If) && context.adjacent_results.fetch(If).annotation
81-
parsed.validate_instance(instance, instance_location, keyword_location, context)
101+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
102+
result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
82103
end
83104
end
84105

85106
class Else < Keyword
107+
def error(formatted_instance_location:, **)
108+
"value at #{formatted_instance_location} does not match conditional `else` schema"
109+
end
110+
86111
def parse
87112
subschema(value)
88113
end
89114

90115
def validate(instance, instance_location, keyword_location, context)
91116
return unless context.adjacent_results.key?(If) && !context.adjacent_results.fetch(If).annotation
92-
parsed.validate_instance(instance, instance_location, keyword_location, context)
117+
subschema_result = parsed.validate_instance(instance, instance_location, keyword_location, context)
118+
result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
93119
end
94120
end
95121

96122
class DependentSchemas < Keyword
123+
def error(formatted_instance_location:, **)
124+
"value at #{formatted_instance_location} does not match applicable `dependentSchemas` schemas"
125+
end
126+
97127
def parse
98128
value.each_with_object({}) do |(key, subschema), out|
99129
out[key] = subschema(subschema, key)
@@ -114,6 +144,10 @@ def validate(instance, instance_location, keyword_location, context)
114144
end
115145

116146
class PrefixItems < Keyword
147+
def error(formatted_instance_location:, **)
148+
"array items at #{formatted_instance_location} do not match corresponding `prefixItems` schemas"
149+
end
150+
117151
def parse
118152
value.map.with_index do |subschema, index|
119153
subschema(subschema, index.to_s)
@@ -132,6 +166,10 @@ def validate(instance, instance_location, keyword_location, context)
132166
end
133167

134168
class Items < Keyword
169+
def error(formatted_instance_location:, **)
170+
"array items at #{formatted_instance_location} do not match `items` schema"
171+
end
172+
135173
def parse
136174
subschema(value)
137175
end
@@ -151,6 +189,10 @@ def validate(instance, instance_location, keyword_location, context)
151189
end
152190

153191
class Contains < Keyword
192+
def error(formatted_instance_location:, **)
193+
"array at #{formatted_instance_location} does not contain enough items that match `contains` schema"
194+
end
195+
154196
def parse
155197
subschema(value)
156198
end
@@ -174,6 +216,10 @@ def validate(instance, instance_location, keyword_location, context)
174216
end
175217

176218
class Properties < Keyword
219+
def error(formatted_instance_location:, **)
220+
"object properties at #{formatted_instance_location} do not match corresponding `properties` schemas"
221+
end
222+
177223
def parse
178224
value.each_with_object({}) do |(property, subschema), out|
179225
out[property] = subschema(subschema, property)
@@ -214,6 +260,10 @@ def validate(instance, instance_location, keyword_location, context)
214260
end
215261

216262
class PatternProperties < Keyword
263+
def error(formatted_instance_location:, **)
264+
"object properties at #{formatted_instance_location} do not match corresponding `patternProperties` schemas"
265+
end
266+
217267
def parse
218268
value.each_with_object({}) do |(pattern, subschema), out|
219269
out[pattern] = subschema(subschema, pattern)
@@ -241,6 +291,14 @@ def validate(instance, instance_location, keyword_location, context)
241291
end
242292

243293
class AdditionalProperties < Keyword
294+
def error(formatted_instance_location:, **)
295+
"object properties at #{formatted_instance_location} do not match `additionalProperties` schema"
296+
end
297+
298+
def false_schema_error(formatted_instance_location:, **)
299+
"object property at #{formatted_instance_location} is not defined and schema does not allow additional properties"
300+
end
301+
244302
def parse
245303
subschema(value)
246304
end
@@ -265,6 +323,10 @@ def validate(instance, instance_location, keyword_location, context)
265323
end
266324

267325
class PropertyNames < Keyword
326+
def error(formatted_instance_location:, **)
327+
"object property names at #{formatted_instance_location} do not match `propertyNames` schema"
328+
end
329+
268330
def parse
269331
subschema(value)
270332
end
@@ -281,6 +343,10 @@ def validate(instance, instance_location, keyword_location, context)
281343
end
282344

283345
class Dependencies < Keyword
346+
def error(formatted_instance_location:, **)
347+
"object at #{formatted_instance_location} either does not match applicable `dependencies` schemas or is missing required `dependencies` properties"
348+
end
349+
284350
def parse
285351
value.each_with_object({}) do |(key, value), out|
286352
out[key] = value.is_a?(Array) ? value : subschema(value, key)

lib/json_schemer/draft202012/vocab/format_annotation.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ class Format < Keyword
1212
true
1313
end
1414

15+
def error(formatted_instance_location:, **)
16+
"value at #{formatted_instance_location} does not match format: #{value}"
17+
end
18+
1519
def parse
1620
root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) }
1721
end

lib/json_schemer/draft202012/vocab/format_assertion.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class Format < Keyword
1010
!instance.is_a?(String) || valid_spec_format?(instance, value)
1111
end
1212

13+
def error(formatted_instance_location:, **)
14+
"value at #{formatted_instance_location} does not match format: #{value}"
15+
end
16+
1317
def parse
1418
root.format && root.formats.fetch(value) { root.meta_schema.formats.fetch(value, DEFAULT_FORMAT) }
1519
end

lib/json_schemer/draft202012/vocab/unevaluated.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module Draft202012
44
module Vocab
55
module Unevaluated
66
class UnevaluatedItems < Keyword
7+
def error(formatted_instance_location:, **)
8+
"array items at #{formatted_instance_location} do not match `unevaluatedItems` schema"
9+
end
10+
711
def parse
812
subschema(value)
913
end
@@ -43,6 +47,10 @@ def collect_unevaluated_items(result, instance_location, unevaluated_items)
4347
end
4448

4549
class UnevaluatedProperties < Keyword
50+
def error(formatted_instance_location:, **)
51+
"object properties at #{formatted_instance_location} do not match `unevaluatedProperties` schema"
52+
end
53+
4654
def parse
4755
subschema(value)
4856
end

0 commit comments

Comments
 (0)