1# ___
2# \./ DANGER: This project implements some code generation
3# .--.O.--. techniques involving string concatenation.
4# \/ \/ If you look at it, you might die.
5#
6
7r"""
8Installation
9************
10
11.. code-block:: bash
12
13 pip install fastjsonschema
14
15Support only for Python 3.3 and higher.
16
17About
18*****
19
20``fastjsonschema`` implements validation of JSON documents by JSON schema.
21The library implements JSON schema drafts 04, 06, and 07. The main purpose is
22to have a really fast implementation. See some numbers:
23
24 * Probably the most popular, ``jsonschema``, can take up to 5 seconds for valid
25 inputs and 1.2 seconds for invalid inputs.
26 * Second most popular, ``json-spec``, is even worse with up to 7.2 and 1.7 seconds.
27 * Last ``validictory``, now deprecated, is much better with 370 or 23 milliseconds,
28 but it does not follow all standards, and it can be still slow for some purposes.
29
30With this library you can gain big improvements as ``fastjsonschema`` takes
31only about 25 milliseconds for valid inputs and 2 milliseconds for invalid ones.
32Pretty amazing, right? :-)
33
34Technically it works by generating the most stupid code on the fly, which is fast but
35is hard to write by hand. The best efficiency is achieved when a validator is compiled
36once and used many times, of course. It works similarly like regular expressions. But
37you can also generate the code to a file, which is even slightly faster.
38
39You can run the performance benchmarks on your computer or server with the included
40script:
41
42.. code-block:: bash
43
44 $ make performance
45 fast_compiled valid ==> 0.0993900
46 fast_compiled invalid ==> 0.0041089
47 fast_compiled_without_exc valid ==> 0.0465258
48 fast_compiled_without_exc invalid ==> 0.0023688
49 fast_file valid ==> 0.0989483
50 fast_file invalid ==> 0.0041104
51 fast_not_compiled valid ==> 11.9572681
52 fast_not_compiled invalid ==> 2.9512092
53 jsonschema valid ==> 5.2233240
54 jsonschema invalid ==> 1.3227916
55 jsonschema_compiled valid ==> 0.4447982
56 jsonschema_compiled invalid ==> 0.0231333
57 jsonspec valid ==> 4.1450569
58 jsonspec invalid ==> 1.0485777
59 validictory valid ==> 0.2730411
60 validictory invalid ==> 0.0183669
61
62This library follows and implements `JSON schema draft-04, draft-06, and draft-07
63<http://json-schema.org>`_. Sometimes it's not perfectly clear, so I recommend also
64check out this `understanding JSON schema <https://spacetelescope.github.io/understanding-json-schema>`_.
65
66Note that there are some differences compared to JSON schema standard:
67
68 * Regular expressions are full Python ones, not only what JSON schema allows. It's easier
69 to allow everything, and also it's faster to compile without limits. So keep in mind that when
70 you will use a more advanced regular expression, it may not work with other libraries or in
71 other languages.
72 * Because Python matches new line for a dollar in regular expressions (``a$`` matches ``a`` and ``a\\n``),
73 instead of ``$`` is used ``\Z`` and all dollars in your regular expression are changed to ``\\Z``
74 as well. When you want to use dollar as regular character, you have to escape it (``\$``).
75 * JSON schema says you can use keyword ``default`` for providing default values. This implementation
76 uses that and always returns transformed input data.
77
78Usage
79*****
80
81.. code-block:: python
82
83 import fastjsonschema
84
85 point_schema = {
86 "type": "object",
87 "properties": {
88 "x": {
89 "type": "number",
90 },
91 "y": {
92 "type": "number",
93 },
94 },
95 "required": ["x", "y"],
96 "additionalProperties": False,
97 }
98
99 point_validator = fastjsonschema.compile(point_schema)
100 try:
101 point_validator({"x": 1.0, "y": 2.0})
102 except fastjsonschema.JsonSchemaException as e:
103 print(f"Data failed validation: {e}")
104
105API
106***
107"""
108from functools import partial, update_wrapper
109
110from .draft04 import CodeGeneratorDraft04
111from .draft06 import CodeGeneratorDraft06
112from .draft07 import CodeGeneratorDraft07
113from .exceptions import JsonSchemaException, JsonSchemaValueException, JsonSchemaValuesException, JsonSchemaDefinitionException
114from .ref_resolver import RefResolver
115from .version import VERSION
116
117__all__ = (
118 'VERSION',
119 'JsonSchemaException',
120 'JsonSchemaValueException',
121 'JsonSchemaValuesException',
122 'JsonSchemaDefinitionException',
123 'validate',
124 'compile',
125 'compile_to_code',
126)
127
128
129def validate(definition, data, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
130 """
131 Validation function for lazy programmers or for use cases when you need
132 to call validation only once, so you do not have to compile it first.
133 Use it only when you do not care about performance (even though it will
134 be still faster than alternative implementations).
135
136 .. code-block:: python
137
138 import fastjsonschema
139
140 fastjsonschema.validate({'type': 'string'}, 'hello')
141 # same as: compile({'type': 'string'})('hello')
142
143 Preferred is to use :any:`compile` function.
144 """
145 return compile(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)(data)
146
147
148#TODO: Change use_default to False when upgrading to version 3.
149# pylint: disable=redefined-builtin,dangerous-default-value,exec-used
150def compile(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
151 """
152 Generates validation function for validating JSON schema passed in ``definition``.
153 Example:
154
155 .. code-block:: python
156
157 import fastjsonschema
158
159 validate = fastjsonschema.compile({'type': 'string'})
160 validate('hello')
161
162 This implementation supports keyword ``default`` (can be turned off
163 by passing `use_default=False`):
164
165 .. code-block:: python
166
167 validate = fastjsonschema.compile({
168 'type': 'object',
169 'properties': {
170 'a': {'type': 'number', 'default': 42},
171 },
172 })
173
174 data = validate({})
175 assert data == {'a': 42}
176
177 Supported implementations are draft-04, draft-06 and draft-07. Which version
178 should be used is determined by `$draft` in your ``definition``. When not
179 specified, the latest implementation is used (draft-07).
180
181 .. code-block:: python
182
183 validate = fastjsonschema.compile({
184 '$schema': 'http://json-schema.org/draft-04/schema',
185 'type': 'number',
186 })
187
188 You can pass mapping from URI to function that should be used to retrieve
189 remote schemes used in your ``definition`` in parameter ``handlers``.
190
191 Also, you can pass mapping for custom formats. Key is the name of your
192 formatter and value can be regular expression, which will be compiled or
193 callback returning `bool` (or you can raise your own exception).
194
195 .. code-block:: python
196
197 validate = fastjsonschema.compile(definition, formats={
198 'foo': r'foo|bar',
199 'bar': lambda value: value in ('foo', 'bar'),
200 })
201
202 Note that formats are automatically used as assertions. It can be turned
203 off by passing `use_formats=False`. When disabled, custom formats are
204 disabled as well. (Added in 2.19.0.)
205
206 If you don't need detailed exceptions, you can turn the details off and gain
207 additional performance by passing `detailed_exceptions=False`.
208
209 By default, the execution stops with the first validation error. If you need
210 to collect all the errors, turn this off by passing `fast_fail=False`.
211
212 Exception :any:`JsonSchemaDefinitionException` is raised when generating the
213 code fails (bad definition).
214
215 Exception :any:`JsonSchemaValueException` is raised from generated function when
216 validation fails (data do not follow the definition).
217
218 Exception :any:`JsonSchemaValuesException` is raised from generated function when
219 validation fails (data do not follow the definition) contatining all the errors
220 (when fast_fail is set to `False`).
221 """
222 resolver, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)
223 global_state = code_generator.global_state
224 # Do not pass local state so it can recursively call itself.
225 exec(code_generator.func_code, global_state)
226 func = global_state[resolver.get_scope_name()]
227 if formats:
228 return update_wrapper(partial(func, custom_formats=formats), func)
229 return func
230
231
232# pylint: disable=dangerous-default-value
233def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
234 """
235 Generates validation code for validating JSON schema passed in ``definition``.
236 Example:
237
238 .. code-block:: python
239
240 import fastjsonschema
241
242 code = fastjsonschema.compile_to_code({'type': 'string'})
243 with open('your_file.py', 'w') as f:
244 f.write(code)
245
246 You can also use it as a script:
247
248 .. code-block:: bash
249
250 echo "{'type': 'string'}" | python3 -m fastjsonschema > your_file.py
251 python3 -m fastjsonschema "{'type': 'string'}" > your_file.py
252
253 Exception :any:`JsonSchemaDefinitionException` is raised when generating the
254 code fails (bad definition).
255 """
256 _, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions, fast_fail)
257 return (
258 'VERSION = "' + VERSION + '"\n' +
259 code_generator.global_state_code + '\n' +
260 code_generator.func_code
261 )
262
263
264def _factory(definition, handlers, formats={}, use_default=True, use_formats=True, detailed_exceptions=True, fast_fail=True):
265 resolver = RefResolver.from_schema(definition, handlers=handlers, store={})
266 code_generator = _get_code_generator_class(definition)(
267 definition,
268 resolver=resolver,
269 formats=formats,
270 use_default=use_default,
271 use_formats=use_formats,
272 detailed_exceptions=detailed_exceptions,
273 fast_fail=fast_fail,
274 )
275 return resolver, code_generator
276
277
278def _get_code_generator_class(schema):
279 # Schema in from draft-06 can be just the boolean value.
280 if isinstance(schema, dict):
281 schema_version = schema.get('$schema', '')
282 if 'draft-04' in schema_version:
283 return CodeGeneratorDraft04
284 if 'draft-06' in schema_version:
285 return CodeGeneratorDraft06
286 return CodeGeneratorDraft07