json-schema 0.1.13 → 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -2,27 +2,40 @@ h1. Ruby JSON Schema Validator
2
2
 
3
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.
4
4
 
5
- h2. Usage
5
+ h2. Dependencies
6
6
 
7
- Install:
7
+ The JSON::Schema library depends on the ruby JSON gem (json). Support for YAJL and other Ruby parsers is planned.
8
+
9
+ h2. Installation
10
+
11
+ From rubygems.org:
8
12
 
9
13
  <pre>
10
14
  gem install json-schema
11
15
  </pre>
12
16
 
13
- If downloading the git repo, build the gem and install it:
17
+ From the git repo:
14
18
 
15
19
  <pre>
16
20
  $ gem build json-schema.gemspec
17
- $ gem install json-schema-0.1.13.gem
21
+ $ gem install json-schema-0.1.14.gem
18
22
  </pre>
19
23
 
24
+
25
+ h2. Basic Usage
26
+
27
+ Two base validation methods exist: <code>validate</code> and <code>validate!</code>. The first returns a boolean on whether a validation attempt passes and the latter will throw a <code>JSON::Schema::ValidationError</code> with an appropriate message/trace on where the validation failed.
28
+
29
+ Both 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.
30
+
31
+ By default, the validator uses (and only supports) 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.
32
+
33
+ h3. Validate Ruby objects against a Ruby schema
34
+
20
35
  <pre>
21
36
  require 'rubygems'
22
37
  require 'json-schema'
23
38
 
24
- JSON::Validator.validate('schema.json', 'data.json')
25
-
26
39
  schema = {
27
40
  "type" => "object",
28
41
  "properties" => {
@@ -35,20 +48,102 @@ data = {
35
48
  }
36
49
 
37
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>
38
71
 
39
- data = [data,data,data]
40
- JSON::Validator.validate(schema, data, :list => true)
72
+ h3. Catch a validation error and print it out
41
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
+
42
85
  data = {
43
86
  "a" => "taco"
44
87
  }
45
88
 
46
89
  begin
47
- JSON::Validator.validate2(schema, data)
48
- rescue ValidationError
90
+ JSON::Validator.validate!(schema, data)
91
+ rescue JSON::Schema::ValidationError
49
92
  puts $!.message
50
93
  end
51
94
  </pre>
95
+
96
+ h3. Extend an existing schema and validating against it
97
+
98
+ 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.
99
+
100
+ <pre>
101
+ require 'rubygems'
102
+ require 'json-schema'
103
+
104
+ class BitwiseAndAttribute < JSON::Schema::Attribute
105
+ def self.validate(current_schema, data, fragments, validator, options = {})
106
+ if data.is_a?(Integer) && data & current_schema.schema['bitwise-and'].to_i == 0
107
+ message = "The property '#{build_fragment(fragments)}' did not evaluate to true when bitwise-AND'd with #{current_schema.schema['bitwise-or']}"
108
+ raise JSON::Schema::ValidationError.new(message, fragments, current_schema)
109
+ end
110
+ end
111
+ end
112
+
113
+ class ExtendedSchema < JSON::Schema::Validator
114
+ def initialize
115
+ super
116
+ extend_schema_definition("http://json-schema.org/draft-03/schema#")
117
+ @attributes["bitwise-and"] = BitwiseAndAttribute
118
+ @uri = URI.parse("http://test.com/test.json")
119
+ end
120
+
121
+ JSON::Validator.register_validator(self.new)
122
+ end
123
+
124
+ schema = {
125
+ "$schema" => "http://test.com/test.json",
126
+ "properties" => {
127
+ "a" => {
128
+ "bitwise-and" => 1
129
+ },
130
+ "b" => {
131
+ "type" => "string"
132
+ }
133
+ }
134
+ }
135
+
136
+ data = {
137
+ "a" => 0
138
+ }
139
+
140
+ data = {"a" => 1, "b" => "taco"}
141
+ JSON::Validator.validate(schema,data) # => true
142
+ data = {"a" => 1, "b" => 5}
143
+ JSON::Validator.validate(schema,data) # => false
144
+ data = {"a" => 0, "b" => "taco"}
145
+ JSON::Validator.validate(schema,data) # => false
146
+ </pre>
52
147
 
53
148
 
54
149
  h2. Notes
@@ -56,7 +151,6 @@ h2. Notes
56
151
  The following core schema attributes are not implemented:
57
152
 
58
153
  * default - This library aims to solely be a validator and does not modify an object it is validating.
59
- * $schema - Support for this will come later, possibly with the ability to validate against other JSON Schema draft versions
60
154
 
61
155
  The 'format' attribute is only validated for the following values:
62
156
 
data/lib/json-schema.rb CHANGED
@@ -2,4 +2,6 @@ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/json-schema"
2
2
 
3
3
  require 'schema'
4
4
  require 'validator'
5
+ Dir[File.join(File.dirname(__FILE__), "json-schema/attributes/*")].each {|file| require file }
6
+ Dir[File.join(File.dirname(__FILE__), "json-schema/validators/*")].each {|file| require file }
5
7
  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
+ raise ValidationError.new(message, fragments, current_schema)
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)
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 outside of the schema when none are allowed"
26
+ raise ValidationError.new(message, fragments, current_schema)
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)
32
+ fragments.pop
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
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) && !data.has_key?(dependency_value)
9
+ message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{dependency_value}'"
10
+ raise ValidationError.new(message, fragments, current_schema)
11
+ elsif dependency_value.is_a?(Array)
12
+ dependency_value.each do |value|
13
+ if !data.has_key?(value)
14
+ message = "The property '#{build_fragment(fragments)}' has a property '#{property}' that depends on a missing property '#{value}'"
15
+ raise ValidationError.new(message, fragments, current_schema)
16
+ end
17
+ end
18
+ else
19
+ schema = JSON::Schema.new(dependency_value,current_schema.uri,validator)
20
+ schema.validate(data, fragments)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ 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})
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
+ raise ValidationError.new(message, fragments, current_schema)
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)}' 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
+ raise ValidationError.new(message, fragments, current_schema)
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)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,105 @@
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
+ error_message = "The property '#{build_fragment(fragments)}' must be a string and be a date/time in the ISO-8601 format of YYYY-MM-DDThh:mm:ssZ"
10
+ raise ValidationError.new(error_message, fragments, current_schema) if !data.is_a?(String)
11
+ r = Regexp.new('^\d\d\d\d-\d\d-\d\dT(\d\d):(\d\d):(\d\d)Z$')
12
+ if (m = r.match(data))
13
+ parts = data.split("T")
14
+ begin
15
+ Date.parse(parts[0])
16
+ rescue Exception
17
+ raise ValidationError.new(error_message, fragments, current_schema)
18
+ end
19
+ begin
20
+ raise ValidationError.new(error_message, fragments, current_schema) if m[1].to_i > 23
21
+ raise ValidationError.new(error_message, fragments, current_schema) if m[2].to_i > 59
22
+ raise ValidationError.new(error_message, fragments, current_schema) if m[3].to_i > 59
23
+ rescue Exception
24
+ raise ValidationError.new(error_message, fragments, current_schema)
25
+ end
26
+ else
27
+ raise ValidationError.new(error_message, fragments, current_schema)
28
+ end
29
+
30
+ # Date in the format of YYYY-MM-DD
31
+ when 'date'
32
+ error_message = "The property '#{build_fragment(fragments)}' must be a string and be a date in the format of YYYY-MM-DD"
33
+ raise ValidationError.new(error_message, fragments, current_schema) if !data.is_a?(String)
34
+ r = Regexp.new('^\d\d\d\d-\d\d-\d\d$')
35
+ if (m = r.match(data))
36
+ begin
37
+ Date.parse(data)
38
+ rescue Exception
39
+ raise ValidationError.new(error_message, fragments, current_schema)
40
+ end
41
+ else
42
+ raise ValidationError.new(error_message, fragments, current_schema)
43
+ end
44
+
45
+ # Time in the format of HH:MM:SS
46
+ when 'time'
47
+ error_message = "The property '#{build_fragment(fragments)}' must be a string and be a time in the format of hh:mm:ss"
48
+ raise ValidationError.new(error_message, fragments, current_schema) if !data.is_a?(String)
49
+ r = Regexp.new('^(\d\d):(\d\d):(\d\d)$')
50
+ if (m = r.match(data))
51
+ raise ValidationError.new(error_message, fragments, current_schema) if m[1].to_i > 23
52
+ raise ValidationError.new(error_message, fragments, current_schema) if m[2].to_i > 59
53
+ raise ValidationError.new(error_message, fragments, current_schema) if m[3].to_i > 59
54
+ else
55
+ raise ValidationError.new(error_message, fragments, current_schema)
56
+ end
57
+
58
+ # IPv4 in dotted-quad format
59
+ when 'ip-address', 'ipv4'
60
+ error_message = "The property '#{build_fragment(fragments)}' must be a string and be a valid IPv4 address"
61
+ raise ValidationError.new(error_message, fragments, current_schema) if !data.is_a?(String)
62
+ r = Regexp.new('^(\d+){1,3}\.(\d+){1,3}\.(\d+){1,3}\.(\d+){1,3}$')
63
+ if (m = r.match(data))
64
+ 1.upto(4) do |x|
65
+ raise ValidationError.new(error_message, fragments, current_schema) if m[x].to_i > 255
66
+ end
67
+ else
68
+ raise ValidationError.new(error_message, fragments, current_schema)
69
+ end
70
+
71
+ # IPv6 in standard format (including abbreviations)
72
+ when 'ipv6'
73
+ error_message = "The property '#{build_fragment(fragments)}' must be a string and be a valid IPv6 address"
74
+ raise ValidationError.new(error_message, fragments, current_schema) if !data.is_a?(String)
75
+ r = Regexp.new('^[a-f0-9:]+$')
76
+ if (m = r.match(data))
77
+ # All characters are valid, now validate structure
78
+ parts = data.split(":")
79
+ raise ValidationError.new(error_message, fragments, current_schema) if parts.length > 8
80
+ condensed_zeros = false
81
+ parts.each do |part|
82
+ if part.length == 0
83
+ raise ValidationError.new(error_message, fragments, current_schema) if condensed_zeros
84
+ condensed_zeros = true
85
+ end
86
+ raise ValidationError.new(error_message, fragments, current_schema) if part.length > 4
87
+ end
88
+ else
89
+ raise ValidationError.new(error_message, fragments, current_schema)
90
+ end
91
+
92
+ # Milliseconds since the epoch. Must be an integer or a float
93
+ when 'utc-millisec'
94
+ error_message = "The property '#{build_fragment(fragments)}' must be an integer or a float"
95
+ raise ValidationError.new(error_message, fragments, current_schema) if (!data.is_a?(Numeric))
96
+
97
+ # Must be a string
98
+ when 'regex','color','style','phone','uri','email','host-name'
99
+ error_message = "The property '#{build_fragment(fragments)}' must be a string"
100
+ raise ValidationError.new(error_message, fragments, current_schema) if (!data.is_a?(String))
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,25 @@
1
+ module JSON
2
+ class Schema
3
+ class ItemsAttribute < Attribute
4
+ def self.validate(current_schema, data, fragments, validator, options = {})
5
+ if data.is_a?(Array)
6
+ if current_schema.schema['items'].is_a?(Hash)
7
+ data.each_with_index do |item,i|
8
+ schema = JSON::Schema.new(current_schema.schema['items'],current_schema.uri,validator)
9
+ fragments << i.to_s
10
+ schema.validate(item,fragments)
11
+ fragments.pop
12
+ end
13
+ elsif current_schema.schema['items'].is_a?(Array)
14
+ current_schema.schema['items'].each_with_index do |item_schema,i|
15
+ schema = JSON::Schema.new(item_schema,current_schema.uri,validator)
16
+ fragments << i.to_s
17
+ schema.validate(data[i],fragments)
18
+ fragments.pop
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end