Skip to content

Commit dd8b982

Browse files
committed
JSON Schema validation in postgres
0 parents  commit dd8b982

File tree

5 files changed

+382
-0
lines changed

5 files changed

+382
-0
lines changed

.travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
sudo: false
2+
install:
3+
- pip install psycopg2
4+
before_script:
5+
psql -c 'CREATE DATABASE json_test;' -U postgres
6+
env:
7+
- DATABASE_URL=postgres:///json_test
8+
services:
9+
- postgresql
10+
addons:
11+
postgresql: "9.5"
12+
script: python test.py
13+
language: python

JSON-Schema-Test-Suite

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 5fb3d9f1a1c4136f544fbd0029942ea559732f8e

jsonschema.sql

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
CREATE OR REPLACE FUNCTION _validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$
2+
BEGIN
3+
IF type = 'integer' THEN
4+
IF jsonb_typeof(data) != 'number' THEN
5+
RETURN false;
6+
END IF;
7+
IF NOT data::text SIMILAR TO '[0-9]+' THEN
8+
RETURN false;
9+
END IF;
10+
ELSE
11+
IF type != jsonb_typeof(data) THEN
12+
RETURN false;
13+
END IF;
14+
END IF;
15+
RETURN true;
16+
END;
17+
$f$ LANGUAGE 'plpgsql';
18+
19+
20+
CREATE OR REPLACE FUNCTION validate_json_schema(schema jsonb, data jsonb, root_schema jsonb DEFAULT NULL) RETURNS boolean AS $f$
21+
DECLARE
22+
prop text;
23+
item jsonb;
24+
path text[];
25+
types text[];
26+
pattern text;
27+
props text[];
28+
BEGIN
29+
IF root_schema IS NULL THEN
30+
root_schema = schema;
31+
END IF;
32+
33+
IF schema ? 'type' THEN
34+
IF jsonb_typeof(schema->'type') = 'array' THEN
35+
types = ARRAY(SELECT jsonb_array_elements_text(schema->'type'));
36+
ELSE
37+
types = ARRAY[schema->>'type'];
38+
END IF;
39+
IF (SELECT NOT bool_or(_validate_json_schema_type(type, data)) FROM unnest(types) type) THEN
40+
RETURN false;
41+
END IF;
42+
END IF;
43+
44+
IF schema ? 'properties' THEN
45+
FOR prop IN SELECT jsonb_object_keys(schema->'properties') LOOP
46+
IF data ? prop AND NOT validate_json_schema(schema->'properties'->prop, data->prop, root_schema) THEN
47+
RETURN false;
48+
END IF;
49+
END LOOP;
50+
END IF;
51+
52+
IF schema ? 'required' AND jsonb_typeof(data) = 'object' THEN
53+
IF NOT ARRAY(SELECT jsonb_object_keys(data)) @>
54+
ARRAY(SELECT jsonb_array_elements_text(schema->'required')) THEN
55+
RETURN false;
56+
END IF;
57+
END IF;
58+
59+
IF schema ? 'items' AND jsonb_typeof(data) = 'array' THEN
60+
IF jsonb_typeof(schema->'items') = 'object' THEN
61+
FOR item IN SELECT jsonb_array_elements(data) LOOP
62+
IF NOT validate_json_schema(schema->'items', item, root_schema) THEN
63+
RETURN false;
64+
END IF;
65+
END LOOP;
66+
ELSE
67+
IF NOT (
68+
SELECT bool_and(i > jsonb_array_length(schema->'items') OR validate_json_schema(schema->'items'->(i::int - 1), elem, root_schema))
69+
FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)
70+
) THEN
71+
RETURN false;
72+
END IF;
73+
END IF;
74+
END IF;
75+
76+
IF jsonb_typeof(schema->'additionalItems') = 'boolean' and NOT (schema->'additionalItems')::text::boolean AND jsonb_typeof(schema->'items') = 'array' THEN
77+
IF jsonb_array_length(data) > jsonb_array_length(schema->'items') THEN
78+
RETURN false;
79+
END IF;
80+
END IF;
81+
82+
IF jsonb_typeof(schema->'additionalItems') = 'object' THEN
83+
IF NOT (
84+
SELECT bool_and(validate_json_schema(schema->'additionalItems', elem, root_schema))
85+
FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)
86+
WHERE i > jsonb_array_length(schema->'items')
87+
) THEN
88+
RETURN false;
89+
END IF;
90+
END IF;
91+
92+
IF schema ? 'minimum' AND jsonb_typeof(data) = 'number' THEN
93+
IF data::text::numeric < (schema->>'minimum')::numeric THEN
94+
RETURN false;
95+
END IF;
96+
END IF;
97+
98+
IF schema ? 'maximum' AND jsonb_typeof(data) = 'number' THEN
99+
IF data::text::numeric > (schema->>'maximum')::numeric THEN
100+
RETURN false;
101+
END IF;
102+
END IF;
103+
104+
IF COALESCE((schema->'exclusiveMinimum')::text::bool, FALSE) THEN
105+
IF data::text::numeric = (schema->>'minimum')::numeric THEN
106+
RETURN false;
107+
END IF;
108+
END IF;
109+
110+
IF COALESCE((schema->'exclusiveMaximum')::text::bool, FALSE) THEN
111+
IF data::text::numeric = (schema->>'maximum')::numeric THEN
112+
RETURN false;
113+
END IF;
114+
END IF;
115+
116+
IF schema ? 'anyOf' THEN
117+
IF NOT (SELECT bool_or(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'anyOf') sub_schema) THEN
118+
RETURN false;
119+
END IF;
120+
END IF;
121+
122+
IF schema ? 'allOf' THEN
123+
IF NOT (SELECT bool_and(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'allOf') sub_schema) THEN
124+
RETURN false;
125+
END IF;
126+
END IF;
127+
128+
IF schema ? 'oneOf' THEN
129+
IF 1 != (SELECT COUNT(*) FROM jsonb_array_elements(schema->'oneOf') sub_schema WHERE validate_json_schema(sub_schema, data, root_schema)) THEN
130+
RETURN false;
131+
END IF;
132+
END IF;
133+
134+
IF COALESCE((schema->'uniqueItems')::text::boolean, false) THEN
135+
IF (SELECT COUNT(*) FROM jsonb_array_elements(data)) != (SELECT count(DISTINCT val) FROM jsonb_array_elements(data) val) THEN
136+
RETURN false;
137+
END IF;
138+
END IF;
139+
140+
IF schema ? 'additionalProperties' AND jsonb_typeof(data) = 'object' THEN
141+
props := ARRAY(
142+
SELECT key
143+
FROM jsonb_object_keys(data) key
144+
WHERE key NOT IN (SELECT jsonb_object_keys(schema->'properties'))
145+
AND NOT EXISTS (SELECT * FROM jsonb_object_keys(schema->'patternProperties') pat WHERE key ~ pat)
146+
);
147+
IF jsonb_typeof(schema->'additionalProperties') = 'boolean' THEN
148+
IF NOT (schema->'additionalProperties')::text::boolean AND jsonb_typeof(data) = 'object' AND NOT props <@ ARRAY(SELECT jsonb_object_keys(schema->'properties')) THEN
149+
RETURN false;
150+
END IF;
151+
ELSEIF NOT (
152+
SELECT bool_and(validate_json_schema(schema->'additionalProperties', data->key, root_schema))
153+
FROM unnest(props) key
154+
) THEN
155+
RETURN false;
156+
END IF;
157+
END IF;
158+
159+
IF schema ? '$ref' THEN
160+
path := ARRAY(
161+
SELECT regexp_replace(regexp_replace(path_part, '~1', '/'), '~0', '~')
162+
FROM UNNEST(regexp_split_to_array(schema->>'$ref', '/')) path_part
163+
);
164+
ASSERT path[1] = '#', 'only refs anchored at the root are supported';
165+
IF NOT validate_json_schema(root_schema #> path[2:array_length(path, 1)], data, root_schema) THEN
166+
RETURN false;
167+
END IF;
168+
END IF;
169+
170+
IF schema ? 'enum' THEN
171+
IF NOT EXISTS (SELECT * FROM jsonb_array_elements(schema->'enum') val WHERE val = data) THEN
172+
RETURN false;
173+
END IF;
174+
END IF;
175+
176+
IF schema ? 'minLength' AND jsonb_typeof(data) = 'string' THEN
177+
IF char_length(data #>> '{}') < (schema->>'minLength')::numeric THEN
178+
RETURN false;
179+
END IF;
180+
END IF;
181+
182+
IF schema ? 'maxLength' AND jsonb_typeof(data) = 'string' THEN
183+
IF char_length(data #>> '{}') > (schema->>'maxLength')::numeric THEN
184+
RETURN false;
185+
END IF;
186+
END IF;
187+
188+
IF schema ? 'not' THEN
189+
IF validate_json_schema(schema->'not', data, root_schema) THEN
190+
RETURN false;
191+
END IF;
192+
END IF;
193+
194+
IF schema ? 'maxProperties' AND jsonb_typeof(data) = 'object' THEN
195+
IF (SELECT count(*) FROM jsonb_object_keys(data)) > (schema->>'maxProperties')::numeric THEN
196+
RETURN false;
197+
END IF;
198+
END IF;
199+
200+
IF schema ? 'minProperties' AND jsonb_typeof(data) = 'object' THEN
201+
IF (SELECT count(*) FROM jsonb_object_keys(data)) < (schema->>'minProperties')::numeric THEN
202+
RETURN false;
203+
END IF;
204+
END IF;
205+
206+
IF schema ? 'maxItems' AND jsonb_typeof(data) = 'array' THEN
207+
IF (SELECT count(*) FROM jsonb_array_elements(data)) > (schema->>'maxItems')::numeric THEN
208+
RETURN false;
209+
END IF;
210+
END IF;
211+
212+
IF schema ? 'minItems' AND jsonb_typeof(data) = 'array' THEN
213+
IF (SELECT count(*) FROM jsonb_array_elements(data)) < (schema->>'minItems')::numeric THEN
214+
RETURN false;
215+
END IF;
216+
END IF;
217+
218+
IF schema ? 'dependencies' THEN
219+
FOR prop IN SELECT jsonb_object_keys(schema->'dependencies') LOOP
220+
IF data ? prop THEN
221+
IF jsonb_typeof(schema->'dependencies'->prop) = 'array' THEN
222+
IF NOT (SELECT bool_and(data ? dep) FROM jsonb_array_elements_text(schema->'dependencies'->prop) dep) THEN
223+
RETURN false;
224+
END IF;
225+
ELSE
226+
IF NOT validate_json_schema(schema->'dependencies'->prop, data, root_schema) THEN
227+
RETURN false;
228+
END IF;
229+
END IF;
230+
END IF;
231+
END LOOP;
232+
END IF;
233+
234+
IF schema ? 'pattern' AND jsonb_typeof(data) = 'string' THEN
235+
IF (data #>> '{}') !~ (schema->>'pattern') THEN
236+
RETURN false;
237+
END IF;
238+
END IF;
239+
240+
IF schema ? 'patternProperties' AND jsonb_typeof(data) = 'object' THEN
241+
FOR prop IN SELECT jsonb_object_keys(data) LOOP
242+
FOR pattern IN SELECT jsonb_object_keys(schema->'patternProperties') LOOP
243+
RAISE NOTICE 'prop %s, pattern %, schema %', prop, pattern, schema->'patternProperties'->pattern;
244+
IF prop ~ pattern AND NOT validate_json_schema(schema->'patternProperties'->pattern, data->prop, root_schema) THEN
245+
RETURN false;
246+
END IF;
247+
END LOOP;
248+
END LOOP;
249+
END IF;
250+
251+
IF schema ? 'multipleOf' AND jsonb_typeof(data) = 'number' THEN
252+
IF data::text::numeric % (schema->>'multipleOf')::numeric != 0 THEN
253+
RETURN false;
254+
END IF;
255+
END IF;
256+
257+
RETURN true;
258+
END;
259+
$f$ LANGUAGE 'plpgsql';

test.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
import json
3+
import sys
4+
5+
import psycopg2
6+
7+
8+
conn = psycopg2.connect(os.environ['DATABASE_URL'])
9+
conn.set_session(autocommit=True)
10+
11+
cur = conn.cursor()
12+
13+
with open('jsonschema.sql') as f:
14+
cur.execute(f.read())
15+
16+
with open('tests.sql') as f:
17+
cur.execute(f.read())
18+
19+
EXCLUDE = {'optional', 'refRemote.json', 'definitions.json'}
20+
21+
os.chdir('JSON-Schema-Test-Suite/tests/draft4')
22+
failures = 0
23+
24+
test_files = sys.argv[1:]
25+
if not test_files:
26+
test_files = [test_file for test_file in os.listdir('.') if test_file not in EXCLUDE]
27+
28+
for test_file in test_files:
29+
with open(test_file) as f:
30+
suites = json.load(f)
31+
for suite in suites:
32+
for test in suite['tests']:
33+
def fail(e):
34+
print("%s: validate_json_schema('%s', '%s')" % (test_file, json.dumps(suite['schema']), json.dumps(test['data'])))
35+
print('Failed: %s: %s. %s' % (suite['description'], test['description'], e))
36+
try:
37+
cur.execute('SELECT validate_json_schema(%s, %s)', (json.dumps(suite['schema']), json.dumps(test['data'])))
38+
except psycopg2.DataError as e:
39+
fail(e)
40+
failures += 1
41+
else:
42+
valid, = cur.fetchone()
43+
if valid != test['valid']:
44+
fail(valid)
45+
failures += 1
46+
47+
sys.exit(failures)

tests.sql

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
CREATE OR REPLACE FUNCTION run_tests() RETURNS boolean AS $f$
2+
BEGIN
3+
ASSERT validate_json_schema('{}', '{}');
4+
ASSERT NOT validate_json_schema('{"type": "object"}', '1');
5+
ASSERT validate_json_schema('{"type": "object"}', '{}');
6+
ASSERT validate_json_schema('{"type": "number"}', '123.1');
7+
ASSERT validate_json_schema('{"type": "number"}', '123');
8+
ASSERT validate_json_schema('{"type": "integer"}', '123');
9+
ASSERT NOT validate_json_schema('{"type": "integer"}', '123.1');
10+
ASSERT NOT validate_json_schema('{"type": "number"}', '"a"');
11+
ASSERT validate_json_schema('{"type": "string"}', '"a"');
12+
ASSERT NOT validate_json_schema('{"type": "string"}', '{}');
13+
ASSERT validate_json_schema('{"type": "boolean"}', 'true');
14+
ASSERT NOT validate_json_schema('{"type": "boolean"}', 'null');
15+
ASSERT validate_json_schema('{"type": "null"}', 'null');
16+
ASSERT NOT validate_json_schema('{"type": "null"}', 'true');
17+
ASSERT validate_json_schema('{"type": "boolean"}', 'false');
18+
ASSERT NOT validate_json_schema('{"type": "boolean"}', '[]');
19+
ASSERT validate_json_schema('{"type": "array"}', '[]');
20+
ASSERT NOT validate_json_schema('{"type": "array"}', '1');
21+
22+
ASSERT validate_json_schema('{"properties": {"foo": {"type": "string"}}}', '{"foo": "bar"}');
23+
ASSERT NOT validate_json_schema('{"properties": {"foo": {"type": "string"}}}', '{"foo": 1}');
24+
ASSERT validate_json_schema('{"properties": {"foo": {"type": "string"}}}', '{}');
25+
26+
ASSERT validate_json_schema('{"required": ["foo"]}', '{"foo": 1}');
27+
ASSERT NOT validate_json_schema('{"required": ["foo"]}', '{"bar": 1}');
28+
29+
ASSERT validate_json_schema('{"items": {"type": "integer"}}', '[1, 2, 3]');
30+
ASSERT NOT validate_json_schema('{"items": {"type": "integer"}}', '[1, 2, 3, "x"]');
31+
32+
ASSERT validate_json_schema('{"minimum": 1.1}', '2.6');
33+
ASSERT NOT validate_json_schema('{"minimum": 1.1}', '0.6');
34+
ASSERT validate_json_schema('{"minimum": 12}', '12');
35+
36+
ASSERT validate_json_schema('{"anyOf": [{"type": "integer"}, {"minimum": 2}]}', '1');
37+
ASSERT validate_json_schema('{"anyOf": [{"type": "integer"}, {"minimum": 2}]}', '2.5');
38+
ASSERT validate_json_schema('{"anyOf": [{"type": "integer"}, {"minimum": 2}]}', '3');
39+
ASSERT NOT validate_json_schema('{"anyOf": [{"type": "integer"}, {"minimum": 2}]}', '1.5');
40+
41+
ASSERT validate_json_schema('{"uniqueItems": true}', '[1, 2]');
42+
ASSERT NOT validate_json_schema('{"uniqueItems": true}', '[1, 1]');
43+
44+
ASSERT validate_json_schema('{"properties": {"foo": {}, "bar": {}}, "additionalProperties": false}', '{"foo": 1}');
45+
ASSERT NOT validate_json_schema('{"properties": {"foo": {}, "bar": {}}, "additionalProperties": false}', '{"foo" : 1, "bar" : 2, "quux" : "boom"}');
46+
47+
ASSERT validate_json_schema('{"properties": {"foo": {"$ref": "#"}}, "additionalProperties": false}', '{"foo": false}');
48+
ASSERT validate_json_schema('{"properties": {"foo": {"$ref": "#"}}, "additionalProperties": false}', '{"foo": {"foo": false}}');
49+
ASSERT NOT validate_json_schema('{"properties": {"foo": {"$ref": "#"}}, "additionalProperties": false}', '{"bar": false}');
50+
ASSERT NOT validate_json_schema('{"properties": {"foo": {"$ref": "#"}}, "additionalProperties": false}', '{"foo": {"bar": false}}');
51+
52+
ASSERT validate_json_schema('{"properties": {"foo": {"type": "integer"}, "bar": {"$ref": "#/properties/foo"}}}', '{"bar": 3}');
53+
ASSERT NOT validate_json_schema('{"properties": {"foo": {"type": "integer"}, "bar": {"$ref": "#/properties/foo"}}}', '{"bar": true}');
54+
55+
ASSERT validate_json_schema('{"enum": [1,2,3]}', '1');
56+
ASSERT NOT validate_json_schema('{"enum": [1,2,3]}', '4');
57+
58+
RETURN true;
59+
END;
60+
$f$ LANGUAGE 'plpgsql';
61+
62+
SELECT run_tests();

0 commit comments

Comments
 (0)