seomoz-json-schema 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/README.textile +216 -0
  2. data/lib/json-schema.rb +13 -0
  3. data/lib/json-schema/attributes/additionalitems.rb +23 -0
  4. data/lib/json-schema/attributes/additionalproperties.rb +39 -0
  5. data/lib/json-schema/attributes/dependencies.rb +30 -0
  6. data/lib/json-schema/attributes/disallow.rb +11 -0
  7. data/lib/json-schema/attributes/divisibleby.rb +16 -0
  8. data/lib/json-schema/attributes/enum.rb +24 -0
  9. data/lib/json-schema/attributes/extends.rb +14 -0
  10. data/lib/json-schema/attributes/format.rb +113 -0
  11. data/lib/json-schema/attributes/items.rb +25 -0
  12. data/lib/json-schema/attributes/maxdecimal.rb +15 -0
  13. data/lib/json-schema/attributes/maximum.rb +15 -0
  14. data/lib/json-schema/attributes/maximum_inclusive.rb +15 -0
  15. data/lib/json-schema/attributes/maxitems.rb +12 -0
  16. data/lib/json-schema/attributes/maxlength.rb +14 -0
  17. data/lib/json-schema/attributes/minimum.rb +15 -0
  18. data/lib/json-schema/attributes/minimum_inclusive.rb +15 -0
  19. data/lib/json-schema/attributes/minitems.rb +12 -0
  20. data/lib/json-schema/attributes/minlength.rb +14 -0
  21. data/lib/json-schema/attributes/pattern.rb +15 -0
  22. data/lib/json-schema/attributes/patternproperties.rb +23 -0
  23. data/lib/json-schema/attributes/properties.rb +23 -0
  24. data/lib/json-schema/attributes/properties_optional.rb +23 -0
  25. data/lib/json-schema/attributes/ref.rb +55 -0
  26. data/lib/json-schema/attributes/type.rb +71 -0
  27. data/lib/json-schema/attributes/uniqueitems.rb +16 -0
  28. data/lib/json-schema/schema.rb +50 -0
  29. data/lib/json-schema/uri/file.rb +32 -0
  30. data/lib/json-schema/uri/uuid.rb +285 -0
  31. data/lib/json-schema/validator.rb +425 -0
  32. data/lib/json-schema/validators/draft1.rb +32 -0
  33. data/lib/json-schema/validators/draft2.rb +33 -0
  34. data/lib/json-schema/validators/draft3.rb +38 -0
  35. data/resources/draft-01.json +155 -0
  36. data/resources/draft-02.json +166 -0
  37. data/resources/draft-03.json +174 -0
  38. data/test/data/bad_data_1.json +3 -0
  39. data/test/data/good_data_1.json +3 -0
  40. data/test/schemas/good_schema_1.json +10 -0
  41. data/test/schemas/good_schema_2.json +10 -0
  42. data/test/test_extended_schema.rb +68 -0
  43. data/test/test_files.rb +35 -0
  44. data/test/test_full_validation.rb +38 -0
  45. data/test/test_jsonschema_draft1.rb +703 -0
  46. data/test/test_jsonschema_draft2.rb +775 -0
  47. data/test/test_jsonschema_draft3.rb +972 -0
  48. data/test/test_schema_validation.rb +43 -0
  49. metadata +137 -0
@@ -0,0 +1,216 @@
1
+ h1. Ruby JSON Schema Validator
2
+
3
+ This library is intended to provide Ruby with an interface for validating JSON objects against a JSON schema conforming to "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03. Legacy support for "JSON Schema Draft 2":http://tools.ietf.org/html/draft-zyp-json-schema-02 and "JSON Schema Draft 1":http://tools.ietf.org/html/draft-zyp-json-schema-01 is also included.
4
+
5
+ h2. Dependencies
6
+
7
+ The JSON::Schema library has no dependencies if the validation methods are called using Ruby objects. However, either the <code>json</code> or the <code>yajl-ruby</code> gem needs to be installed to validate JSON strings or files containing JSON data.
8
+
9
+ h2. Installation
10
+
11
+ From rubygems.org:
12
+
13
+ <pre>
14
+ gem install json-schema
15
+ </pre>
16
+
17
+ From the git repo:
18
+
19
+ <pre>
20
+ $ gem build json-schema.gemspec
21
+ $ gem install json-schema-1.0.1.gem
22
+ </pre>
23
+
24
+
25
+ h2. Usage
26
+
27
+ Three base validation methods exist: <code>validate</code>, <code>validate!</code>, and <code>fully_validate</code>. The first returns a boolean on whether a validation attempt passes and the second will throw a <code>JSON::Schema::ValidationError</code> with an appropriate message/trace on where the validation failed. The third validation method does not immediately fail upon a validation error and instead builds an array of validation errors return when validation is complete.
28
+
29
+ All methods take two arguments, which can be either a JSON string, a file containing JSON, or a Ruby object representing JSON data. The first argument to these methods is always the schema, the second is always the data to validate. An optional third options argument is also accepted; available options are used in the examples below.
30
+
31
+ By default, the validator uses the "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03 specification for validation; however, the user is free to specify additional specifications or extend existing ones. Legacy support for Draft 1 and Draft 2 is included by either passing an optional <code>:version</code> parameter to the <code>validate</code> method (set either as <code>:draft1</code> or <code>draft2</code>), or by declaring the <code>$schema</code> attribute in the schema and referencing the appropriate specification URI. Note that the <code>$schema</code> attribute takes precedence over the <code>:version</code> option during parsing and validation.
32
+
33
+ h3. Validate Ruby objects against a Ruby schema
34
+
35
+ <pre>
36
+ require 'rubygems'
37
+ require 'json-schema'
38
+
39
+ schema = {
40
+ "type" => "object",
41
+ "properties" => {
42
+ "a" => {"type" => "integer", "required" => true}
43
+ }
44
+ }
45
+
46
+ data = {
47
+ "a" => 5
48
+ }
49
+
50
+ JSON::Validator.validate(schema, data)
51
+ </pre>
52
+
53
+ h3. Validate a JSON string against a JSON schema file
54
+
55
+ <pre>
56
+ require 'rubygems'
57
+ require 'json-schema'
58
+
59
+ JSON::Validator.validate('schema.json', '{"a" : 5}')
60
+ </pre>
61
+
62
+ h3. Validate a list of objects against a schema that represents the individual objects
63
+
64
+ <pre>
65
+ require 'rubygems'
66
+ require 'json-schema'
67
+
68
+ data = ['user','user','user']
69
+ JSON::Validator.validate('user.json', data, :list => true)
70
+ </pre>
71
+
72
+ h3. Catch a validation error and print it out
73
+
74
+ <pre>
75
+ require 'rubygems'
76
+ require 'json-schema'
77
+
78
+ schema = {
79
+ "type" => "object",
80
+ "properties" => {
81
+ "a" => {"type" => "integer", "required" => true}
82
+ }
83
+ }
84
+
85
+ data = {
86
+ "a" => "taco"
87
+ }
88
+
89
+ begin
90
+ JSON::Validator.validate!(schema, data)
91
+ rescue JSON::Schema::ValidationError
92
+ puts $!.message
93
+ end
94
+ </pre>
95
+
96
+ h3. Validate a JSON object against a JSON schema object, while also validating the schema itself
97
+
98
+ <pre>
99
+ require 'rubygems'
100
+ require 'json-schema'
101
+
102
+ schema = {
103
+ "type" => "object",
104
+ "properties" => {
105
+ "a" => {"type" => "integer", "required" => "true"} # This will fail schema validation!
106
+ }
107
+ }
108
+
109
+ data = {
110
+ "a" => 5
111
+ }
112
+
113
+ JSON::Validator.validate(schema, data, :validate_schema => true)
114
+ </pre>
115
+
116
+ h3. Validate an object against a JSON Schema Draft 2 schema
117
+
118
+ <pre>
119
+ require 'rubygems'
120
+ require 'json-schema'
121
+
122
+ schema = {
123
+ "type" => "object",
124
+ "properties" => {
125
+ "a" => {"type" => "integer", "optional" => true}
126
+ }
127
+ }
128
+
129
+ data = {
130
+ "a" => 5
131
+ }
132
+
133
+ JSON::Validator.validate(schema, data, :version => :draft2)
134
+ </pre>
135
+
136
+
137
+ h3. Extend an existing schema and validate against it
138
+
139
+ For this example, we are going to extend the "JSON Schema Draft 3":http://tools.ietf.org/html/draft-zyp-json-schema-03 specification by adding a 'bitwise-and' property for validation.
140
+
141
+ <pre>
142
+ require 'rubygems'
143
+ require 'json-schema'
144
+
145
+ class BitwiseAndAttribute < JSON::Schema::Attribute
146
+ def self.validate(current_schema, data, fragments, validator, options = {})
147
+ if data.is_a?(Integer) && data & current_schema.schema['bitwise-and'].to_i == 0
148
+ message = "The property '#{build_fragment(fragments)}' did not evaluate to true when bitwise-AND'd with #{current_schema.schema['bitwise-or']}"
149
+ raise JSON::Schema::ValidationError.new(message, fragments, current_schema)
150
+ end
151
+ end
152
+ end
153
+
154
+ class ExtendedSchema < JSON::Schema::Validator
155
+ def initialize
156
+ super
157
+ extend_schema_definition("http://json-schema.org/draft-03/schema#")
158
+ @attributes["bitwise-and"] = BitwiseAndAttribute
159
+ @uri = URI.parse("http://test.com/test.json")
160
+ end
161
+
162
+ JSON::Validator.register_validator(self.new)
163
+ end
164
+
165
+ schema = {
166
+ "$schema" => "http://test.com/test.json",
167
+ "properties" => {
168
+ "a" => {
169
+ "bitwise-and" => 1
170
+ },
171
+ "b" => {
172
+ "type" => "string"
173
+ }
174
+ }
175
+ }
176
+
177
+ data = {
178
+ "a" => 0
179
+ }
180
+
181
+ data = {"a" => 1, "b" => "taco"}
182
+ JSON::Validator.validate(schema,data) # => true
183
+ data = {"a" => 1, "b" => 5}
184
+ JSON::Validator.validate(schema,data) # => false
185
+ data = {"a" => 0, "b" => "taco"}
186
+ JSON::Validator.validate(schema,data) # => false
187
+ </pre>
188
+
189
+ h2. JSON Backends
190
+
191
+ The JSON::Schema library currently supports the <code>json</code> and <code>yajl-ruby</code> backend JSON parsers. If either of these libraries are installed, they will be automatically loaded and used to parse any JSON strings supplied by the user.
192
+
193
+ If more than one of the supported JSON backends are installed, the <code>yajl-ruby</code> parser is used by default. This can be changed by issuing the following before validation:
194
+
195
+ <pre>
196
+ JSON::Validator.json_backend = :json
197
+ </pre>
198
+
199
+
200
+ h2. Notes
201
+
202
+ The following core schema attributes are not implemented:
203
+
204
+ * default - This library aims to solely be a validator and does not modify an object it is validating.
205
+
206
+ The 'format' attribute is only validated for the following values:
207
+
208
+ * date-time
209
+ * date
210
+ * time
211
+ * ip-address
212
+ * ipv6
213
+
214
+ All other 'format' attribute values are simply checked to ensure the instance value is of the correct datatype (e.g., an instance value is validated to be an integer or a float in the case of 'utc-millisec').
215
+
216
+ Additionally, JSON::Validator does not handle any json hyperschema attributes.
@@ -0,0 +1,13 @@
1
+ require 'multi_json'
2
+ # Force MultiJson to load an engine before we define the JSON constant here; otherwise,
3
+ # it looks for things that are under the JSON namespace that aren't there (since we have defined it here)
4
+ MultiJson.engine
5
+
6
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/json-schema"
7
+
8
+ require 'rubygems'
9
+ require 'schema'
10
+ require 'validator'
11
+ Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/*.rb")].each {|file| require file }
12
+ Dir[File.join(File.dirname(__FILE__), "json-schema/validators/*.rb")].each {|file| require file }
13
+ require 'uri/file'
@@ -0,0 +1,23 @@
1
+ module JSON
2
+ class Schema
3
+ class AdditionalItemsAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Array) && current_schema.schema['items'].is_a?(Array)
6
+ if current_schema.schema['additionalItems'] == false && current_schema.schema['items'].length != data.length
7
+ message = "The property '#{build_fragment(fragments)}' contains additional array elements outside of the schema when none are allowed"
8
+ validation_error(message, fragments, current_schema, options[:record_errors])
9
+ elsif current_schema.schema['additionalItems'].is_a?(Hash)
10
+ schema = JSON::Schema.new(current_schema.schema['additionalItems'],current_schema.uri,validator)
11
+ data.each_with_index do |item,i|
12
+ if i >= current_schema.schema['items'].length
13
+ fragments << i.to_s
14
+ schema.validate(item, fragments, options)
15
+ fragments.pop
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ module JSON
2
+ class Schema
3
+ class AdditionalPropertiesAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Hash)
6
+ extra_properties = data.keys
7
+
8
+ if current_schema.schema['properties']
9
+ extra_properties = extra_properties - current_schema.schema['properties'].keys
10
+ end
11
+
12
+ if current_schema.schema['patternProperties']
13
+ current_schema.schema['patternProperties'].each_key do |key|
14
+ r = Regexp.new(key)
15
+ extras_clone = extra_properties.clone
16
+ extras_clone.each do |prop|
17
+ if r.match(prop)
18
+ extra_properties = extra_properties - [prop]
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ if current_schema.schema['additionalProperties'] == false && !extra_properties.empty?
25
+ message = "The property '#{build_fragment(fragments)}' contains additional properties #{extra_properties.inspect} outside of the schema when none are allowed"
26
+ validation_error(message, fragments, current_schema, options[:record_errors])
27
+ elsif current_schema.schema['additionalProperties'].is_a?(Hash)
28
+ extra_properties.each do |key|
29
+ schema = JSON::Schema.new(current_schema.schema['additionalProperties'],current_schema.uri,validator)
30
+ fragments << key
31
+ schema.validate(data[key],fragments,options)
32
+ fragments.pop
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ module JSON
2
+ class Schema
3
+ class DependenciesAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Hash)
6
+ current_schema.schema['dependencies'].each do |property,dependency_value|
7
+ if data.has_key?(property)
8
+ if dependency_value.is_a?(String)
9
+ if !data.has_key?(dependency_value)
10
+ message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{dependency_value}'"
11
+ validation_error(message, fragments, current_schema, options[:record_errors])
12
+ end
13
+ elsif dependency_value.is_a?(Array)
14
+ dependency_value.each do |value|
15
+ if !data.has_key?(value)
16
+ message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{value}'"
17
+ validation_error(message, fragments, current_schema, options[:record_errors])
18
+ end
19
+ end
20
+ else
21
+ schema = JSON::Schema.new(dependency_value,current_schema.uri,validator)
22
+ schema.validate(data, fragments, options)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module JSON
2
+ class Schema
3
+ class DisallowAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if validator.attributes['type']
6
+ validator.attributes['type'].validate(current_schema, data, fragments, validator, {:disallow => true}.merge(options))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module JSON
2
+ class Schema
3
+ class DivisibleByAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Numeric)
6
+ if current_schema.schema['divisibleBy'] == 0 ||
7
+ current_schema.schema['divisibleBy'] == 0.0 ||
8
+ (BigDecimal.new(data.to_s) % BigDecimal.new(current_schema.schema['divisibleBy'].to_s)).to_f != 0
9
+ message = "The property '#{build_fragment(fragments)}' was not divisible by #{current_schema.schema['divisibleBy']}"
10
+ validation_error(message, fragments, current_schema, options[:record_errors])
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module JSON
2
+ class Schema
3
+ class EnumAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if !current_schema.schema['enum'].include?(data)
6
+ message = "The property '#{build_fragment(fragments)}' value #{data.inspect} did not match one of the following values:"
7
+ current_schema.schema['enum'].each {|val|
8
+ if val.is_a?(NilClass)
9
+ message += " null,"
10
+ elsif val.is_a?(Array)
11
+ message += " (array),"
12
+ elsif val.is_a?(Hash)
13
+ message += " (object),"
14
+ else
15
+ message += " #{val.to_s},"
16
+ end
17
+ }
18
+ message.chop!
19
+ validation_error(message, fragments, current_schema, options[:record_errors])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module JSON
2
+ class Schema
3
+ class ExtendsAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ schemas = current_schema.schema['extends']
6
+ schemas = [schemas] if !schemas.is_a?(Array)
7
+ schemas.each do |s|
8
+ schema = JSON::Schema.new(s,current_schema.uri,validator)
9
+ schema.validate(data, fragments, options)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,113 @@
1
+ module JSON
2
+ class Schema
3
+ class FormatAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ case current_schema.schema['format']
6
+
7
+ # Timestamp in restricted ISO-8601 YYYY-MM-DDThh:mm:ssZ
8
+ when 'date-time'
9
+ if data.is_a?(String)
10
+ error_message = "The property '#{build_fragment(fragments)}' must be a date/time in the ISO-8601 format of YYYY-MM-DDThh:mm:ssZ"
11
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if !data.is_a?(String)
12
+ r = Regexp.new('^\d\d\d\d-\d\d-\d\dT(\d\d):(\d\d):(\d\d)Z$')
13
+ if (m = r.match(data))
14
+ parts = data.split("T")
15
+ begin
16
+ Date.parse(parts[0])
17
+ rescue Exception
18
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
19
+ return
20
+ end
21
+ begin
22
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[1].to_i > 23
23
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[2].to_i > 59
24
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[3].to_i > 59
25
+ rescue Exception
26
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
27
+ return
28
+ end
29
+ else
30
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
31
+ return
32
+ end
33
+ end
34
+
35
+ # Date in the format of YYYY-MM-DD
36
+ when 'date'
37
+ if data.is_a?(String)
38
+ error_message = "The property '#{build_fragment(fragments)}' must be a date in the format of YYYY-MM-DD"
39
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if !data.is_a?(String)
40
+ r = Regexp.new('^\d\d\d\d-\d\d-\d\d$')
41
+ if (m = r.match(data))
42
+ begin
43
+ Date.parse(data)
44
+ rescue Exception
45
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
46
+ return
47
+ end
48
+ else
49
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
50
+ return
51
+ end
52
+ end
53
+
54
+ # Time in the format of HH:MM:SS
55
+ when 'time'
56
+ if data.is_a?(String)
57
+ error_message = "The property '#{build_fragment(fragments)}' must be a time in the format of hh:mm:ss"
58
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if !data.is_a?(String)
59
+ r = Regexp.new('^(\d\d):(\d\d):(\d\d)$')
60
+ if (m = r.match(data))
61
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[1].to_i > 23
62
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[2].to_i > 59
63
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[3].to_i > 59
64
+ else
65
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
66
+ return
67
+ end
68
+ end
69
+
70
+ # IPv4 in dotted-quad format
71
+ when 'ip-address', 'ipv4'
72
+ if data.is_a?(String)
73
+ error_message = "The property '#{build_fragment(fragments)}' must be a valid IPv4 address"
74
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if !data.is_a?(String)
75
+ r = Regexp.new('^(\d+){1,3}\.(\d+){1,3}\.(\d+){1,3}\.(\d+){1,3}$')
76
+ if (m = r.match(data))
77
+ 1.upto(4) do |x|
78
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if m[x].to_i > 255
79
+ end
80
+ else
81
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
82
+ return
83
+ end
84
+ end
85
+
86
+ # IPv6 in standard format (including abbreviations)
87
+ when 'ipv6'
88
+ if data.is_a?(String)
89
+ error_message = "The property '#{build_fragment(fragments)}' must be a valid IPv6 address"
90
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if !data.is_a?(String)
91
+ r = Regexp.new('^[a-f0-9:]+$')
92
+ if (m = r.match(data))
93
+ # All characters are valid, now validate structure
94
+ parts = data.split(":")
95
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if parts.length > 8
96
+ condensed_zeros = false
97
+ parts.each do |part|
98
+ if part.length == 0
99
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if condensed_zeros
100
+ condensed_zeros = true
101
+ end
102
+ validation_error(error_message, fragments, current_schema, options[:record_errors]) and return if part.length > 4
103
+ end
104
+ else
105
+ validation_error(error_message, fragments, current_schema, options[:record_errors])
106
+ return
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end