├── .gitignore ├── LICENSE ├── MANIFEST ├── MANIFEST.in ├── README.md ├── USAGE.md ├── bin └── jsonschema_generator.py ├── json_schema_generator ├── __init__.py ├── generator.py ├── recorder.py ├── schema_types.py ├── test_template.py.tmpl └── validator.py ├── requirements.txt ├── run_tests.py ├── setup.py ├── test_requirements.txt ├── tests ├── __init__.py ├── fixtures.py ├── helpers.py ├── test_generator.py ├── test_integration.py ├── test_recorder.py ├── test_types.py ├── test_validator.py └── tmp │ └── .gitignore └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | 28 | # Translations 29 | *.mo 30 | 31 | # Mr Developer 32 | .mr.developer.cfg 33 | .project 34 | .pydevproject 35 | 36 | # Vim 37 | *.sw? 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Felipe Ramos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README 3 | setup.py 4 | bin/jsonschema_generator.py 5 | json_schema_generator/__init__.py 6 | json_schema_generator/generator.py 7 | json_schema_generator/recorder.py 8 | json_schema_generator/schema_types.py 9 | json_schema_generator/test_template.py.tmpl 10 | json_schema_generator/validator.py 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include json_schema_generator/test_template.py.tmpl 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### JSON Schema Generator 2 | 3 | #### About 4 | 5 | It is a json schema generator from any json source. 6 | 7 | #### Usage 8 | 9 | See [Usage](USAGE.md) 10 | 11 | #### Example 12 | 13 | Since you have a json file with the above structure: 14 | ```json 15 | { 16 | "item_1": "string_value_1", 17 | "item_2": 123, 18 | "item_3": [1, 2, 3], 19 | "item_4": true, 20 | "item_5": null, 21 | "item_6": { "key": "value"}, 22 | "item_7": { 23 | "item_7.1": "string_value_1", 24 | "item_7.2": 123, 25 | "item_7.3": [1, 2, 3], 26 | "item_7.4": true, 27 | "item_7.5": null, 28 | "item_7.6": { "key": "value"} 29 | } 30 | } 31 | ``` 32 | It should generate a json schema as": 33 | ```json 34 | { 35 | "$schema": "http://json-schema.org/draft-03/schema", 36 | "id": "#", 37 | "required": true, 38 | "type": "object", 39 | "properties": { 40 | "item_1": { 41 | "id": "item_1", 42 | "required": true, 43 | "type": "string" 44 | }, 45 | "item_2": { 46 | "id": "item_2", 47 | "required": true, 48 | "type": "number" 49 | }, 50 | "item_3": { 51 | "id": "item_3", 52 | "required": true, 53 | "type": "array" , 54 | "items": { 55 | "id": "0", 56 | "required": true, 57 | "type": "number" 58 | } 59 | }, 60 | "item_4": { 61 | "id": "item_4", 62 | "required": true, 63 | "type": "boolean" 64 | }, 65 | "item_5": { 66 | "id": "item_5", 67 | "required": true, 68 | "type": "null" 69 | }, 70 | "item_6": { 71 | "id": "item_6", 72 | "required": true, 73 | "type": "object" , 74 | "properties": { 75 | "key": { 76 | "id": "key", 77 | "required": true, 78 | "type": "string" 79 | } 80 | } 81 | }, 82 | "item_7": { 83 | "id": "item_7", 84 | "required": true, 85 | "type": "object" , 86 | "properties": { 87 | "item_7.1": { 88 | "id": "item_7.1", 89 | "required": true, 90 | "type": "string" 91 | }, 92 | "item_7.2": { 93 | "id": "item_7.2", 94 | "required": true, 95 | "type": "number" 96 | }, 97 | "item_7.3": { 98 | "id": "item_7.3", 99 | "required": true, 100 | "type": "array" , 101 | "items": { 102 | "id": "0", 103 | "required": true, 104 | "type": "number" 105 | } 106 | }, 107 | "item_7.4": { 108 | "id": "item_7.4", 109 | "required": true, 110 | "type": "boolean" 111 | }, 112 | "item_7.5": { 113 | "id": "item_7.5", 114 | "required": true, 115 | "type": "null" 116 | }, 117 | "item_7.6": { 118 | "id": "item_7.6", 119 | "required": true, 120 | "type": "object" , 121 | "properties": { 122 | "key": { 123 | "id": "key", 124 | "required": true, 125 | "type": "string" 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | ``` 134 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # Json Schema Generator 2 | 3 | 4 | ## Usage: 5 | 6 | ### To record 7 | 8 | $ json_schema_generator.py record http://somewhere.com/any.json file.json_schema 9 | 10 | It creates file.json_schema file containing the generated json schema 11 | 12 | 13 | ### To validate 14 | 15 | $ json_schema_generator.py validate http://somewhere.com/any.json file.json_schema 16 | 17 | It validates if the json validates with file.json_schema 18 | 19 | 20 | ### To create automatic test 21 | 22 | $ json_schema_generator.py homologate http://somewhere.com/immutable.json immutable 23 | 24 | It should **create (or replace)** 25 | a fixture file called **json_schemas/immutable.json_schema** 26 | and a test file called **test_immutable_jsonschema.py** 27 | in the current dir. 28 | 29 | If you want to create it in another path use **--path /wanted/path/dir/** 30 | 31 | -------------------------------------------------------------------------------- /bin/jsonschema_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | #import re 5 | import os 6 | import argparse 7 | import string 8 | 9 | import json_schema_generator 10 | from json_schema_generator import Recorder, Validator 11 | 12 | 13 | def record(args): 14 | if(os.path.isfile(args.json_source)): 15 | rec = Recorder.from_file(args.json_source) 16 | else: 17 | rec = Recorder.from_url(args.json_source) 18 | rec.save_json_schema(args.json_schema_file_path, indent=4) 19 | 20 | 21 | def validate(args): 22 | from six.moves.urllib.request import urlopen 23 | 24 | json_data = urlopen(args.json_source).read() 25 | validator = Validator.from_path(args.json_schema_file_path) 26 | is_valid = validator.assert_json(json_data) 27 | 28 | if is_valid: 29 | print (" * JSON is valid") 30 | else: 31 | print (" ! JSON is broken ") 32 | print (validator.error_message) 33 | 34 | 35 | def homologate(args): 36 | template_file_path = os.path.join(os.path.dirname(json_schema_generator.__file__), 'test_template.py.tmpl') 37 | json_schemas_dir = os.path.join(args.path, 'json_schemas') 38 | json_schema_file_name = '%s.json_schema' % args.homologation_name 39 | json_schema_file_path = os.path.join(json_schemas_dir, json_schema_file_name) 40 | test_file_path = os.path.join(args.path, 'test_%s_json_schema.py' % args.homologation_name) 41 | 42 | with open(template_file_path) as template_file: 43 | tmpl = string.Template(template_file.read()) 44 | 45 | if not os.path.exists(json_schemas_dir): 46 | os.mkdir(json_schemas_dir) 47 | 48 | if not os.path.exists(json_schema_file_path): 49 | rec = Recorder.from_url(args.json_source) 50 | rec.save_json_schema(json_schema_file_path, indent=4) 51 | 52 | rendered = tmpl.substitute( 53 | homologation_name=args.homologation_name, 54 | service_url=args.json_source, 55 | json_schema_file_name=json_schema_file_name, 56 | json_schemas_dir=json_schemas_dir 57 | ) 58 | 59 | with open(test_file_path, 'w') as test_file: 60 | test_file.write(rendered) 61 | 62 | 63 | def main(): 64 | parser = argparse.ArgumentParser() 65 | 66 | default_parser = argparse.ArgumentParser(add_help=False) 67 | default_parser.add_argument('json_source', type=str, help='url or file') 68 | default_parser.add_argument('--path', dest='path', default='', help='set path') 69 | 70 | subparsers = parser.add_subparsers(help='sub-command help') 71 | 72 | parser_record = subparsers.add_parser('record', parents=[default_parser]) 73 | parser_record.add_argument('json_schema_file_path', type=str, help='json schema file path') 74 | parser_record.set_defaults(func=record) 75 | 76 | parser_validate = subparsers.add_parser('validate', parents=[default_parser]) 77 | parser_validate.add_argument('json_schema_file_path', type=str, help='json schema file path') 78 | parser_validate.set_defaults(func=validate) 79 | 80 | parser_homologate = subparsers.add_parser('homologate', parents=[default_parser]) 81 | parser_homologate.add_argument('homologation_name', type=str, help='json schema file path') 82 | parser_homologate.set_defaults(func=homologate) 83 | 84 | args = parser.parse_args() 85 | try: 86 | args.func 87 | except AttributeError: 88 | import sys 89 | print("missing 1 or more required arguments (see '%s --help')" % sys.argv[0]) 90 | exit(1) 91 | else: 92 | args.func(args) 93 | 94 | if __name__ == '__main__': 95 | main() 96 | -------------------------------------------------------------------------------- /json_schema_generator/__init__.py: -------------------------------------------------------------------------------- 1 | from .generator import * 2 | from .validator import * 3 | from .recorder import * 4 | -------------------------------------------------------------------------------- /json_schema_generator/generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | from .schema_types import Type, ObjectType, ArrayType, NullType 5 | 6 | 7 | def json_path(obj, *args): 8 | if not obj: 9 | return None 10 | 11 | for arg in args: 12 | if arg not in obj: 13 | return None 14 | obj = obj[arg] 15 | return obj 16 | 17 | class SchemaGenerator(object): 18 | 19 | def __init__(self, base_object): 20 | """docstring for __init__""" 21 | 22 | self._base_object = base_object 23 | 24 | @property 25 | def base_object(self): 26 | """docstring for base_object""" 27 | 28 | return self._base_object 29 | 30 | @classmethod 31 | def from_json(cls, base_json): 32 | """docstring for from_json""" 33 | 34 | base_object = json.loads(base_json) 35 | obj = cls(base_object) 36 | 37 | return obj 38 | 39 | def to_dict(self, base_object=None, object_id=None, first_level=True, required = True, nullable = False): 40 | """docstring for to_dict""" 41 | 42 | schema_dict = {} 43 | 44 | if first_level: 45 | base_object = self.base_object 46 | schema_dict["$schema"] = Type.schema_version 47 | schema_dict["id"] = "#" 48 | 49 | if object_id is not None: 50 | schema_dict["id"] = str(object_id) 51 | 52 | base_object_type = type(base_object) 53 | schema_type = Type.get_schema_type_for(base_object_type) 54 | 55 | schema_dict["required"] = required 56 | if nullable: 57 | schema_dict["type"] = [ schema_type.json_type, NullType.json_type ] 58 | else: 59 | schema_dict["type"] = schema_type.json_type 60 | 61 | if schema_type == ObjectType and len(base_object) > 0: 62 | schema_dict["properties"] = {} 63 | 64 | for prop, value in base_object.items(): 65 | schema_dict["properties"][prop] = self.to_dict(value, prop, False, required, nullable) 66 | 67 | elif schema_type == ArrayType and len(base_object) > 0: 68 | first_item_type = type(base_object[0]) 69 | same_type = all((type(item) == first_item_type for item in base_object)) 70 | 71 | if same_type: 72 | schema_dict['items'] = self.to_dict(base_object[0], 0, False, required, nullable) 73 | 74 | else: 75 | schema_dict['items'] = [] 76 | 77 | for idx, item in enumerate(base_object): 78 | schema_dict['items'].append(self.to_dict(item, idx, False, required, nullable)) 79 | 80 | return schema_dict 81 | 82 | @classmethod 83 | def set_required(cls, schema_dict, full_path, required, nullable = False): 84 | obj = schema_dict['properties'] 85 | for path_part in full_path[:-1]: 86 | if obj[path_part]['type'] == ArrayType.json_type or obj[path_part]['type'] == [ ArrayType.json_type, NullType.json_type ]: 87 | obj = obj[path_part]['items']['properties'] 88 | else: 89 | obj = obj[path_part]['properties'] 90 | 91 | obj = obj[full_path[-1]] 92 | 93 | obj['required'] = required 94 | 95 | if nullable: 96 | if obj['type'] == ArrayType.json_type and json_path(obj, 'items', 'type'): 97 | obj['items']['type'] = [ obj['items']['type'], NullType.json_type ] 98 | 99 | if isinstance(obj['type'], list): 100 | if NullType.json_type not in obj['type']: 101 | obj['type'].append(NullType.json_type) 102 | else: 103 | obj['type'] = [ obj['type'], NullType.json_type ] 104 | else: 105 | if obj['type'] == ArrayType.json_type and json_path(obj, 'items', 'type'): 106 | if isinstance(obj['items']['type'], list) and len(obj['items']['type']) > 1: 107 | obj['items']['type'] = [ x for x in obj['items']['type'] if x != NullType.json_type ] 108 | if len(obj['items']['type']) == 1: 109 | obj['items']['type'] = obj['items']['type'][0] 110 | 111 | if isinstance(obj['type'], list) and NullType.json_type in obj['type']: 112 | obj['type'] = obj['type'][0] 113 | 114 | 115 | def to_json(self, **kwargs): 116 | required = kwargs.pop('required', True) 117 | nullable = kwargs.pop('nullable', False) 118 | return json.dumps(self.to_dict(required=required, nullable=nullable), **kwargs) 119 | 120 | -------------------------------------------------------------------------------- /json_schema_generator/recorder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from json_schema_generator.generator import SchemaGenerator 4 | 5 | 6 | class Recorder(object): 7 | 8 | def __init__(self, generator): 9 | self.generator = generator 10 | 11 | @classmethod 12 | def from_url(cls, url): 13 | from six.moves.urllib.request import urlopen 14 | 15 | json_data = urlopen(url).read() 16 | generator = SchemaGenerator.from_json(json_data) 17 | 18 | return cls(generator) 19 | 20 | @classmethod 21 | def from_file(cls, path): 22 | with open(path, 'rb') as f: 23 | json_data = f.read() 24 | generator = SchemaGenerator.from_json(json_data) 25 | 26 | return cls(generator) 27 | 28 | def save_json_schema(self, file_path, **kwargs): 29 | json_schema_data = self.generator.to_json(**kwargs) 30 | 31 | with open(file_path, 'w') as json_schema_file: 32 | json_schema_file.write(json_schema_data) 33 | -------------------------------------------------------------------------------- /json_schema_generator/schema_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | 5 | 6 | class Type(object): 7 | 8 | schema_version = u"http://json-schema.org/draft-03/schema#" 9 | json_type = None 10 | id = None 11 | required = False 12 | 13 | @classmethod 14 | def get_schema_type_for(self, t): 15 | """docstring for get_schema_type_for""" 16 | 17 | schema_type = SCHEMA_TYPES.get(t) 18 | 19 | if not schema_type: 20 | raise JsonSchemaTypeNotFound( 21 | "There is no schema type for %s.\n Try:\n %s" % ( 22 | str(t), ",\n".join(["\t%s" % str(k) for k in SCHEMA_TYPES.keys()]) 23 | ) 24 | ) 25 | 26 | return schema_type 27 | 28 | 29 | class NumberType(object): 30 | json_type = "number" 31 | 32 | 33 | class StringType(object): 34 | json_type = "string" 35 | 36 | 37 | class NullType(object): 38 | json_type = "null" 39 | 40 | 41 | class BooleanType(object): 42 | json_type = "boolean" 43 | 44 | 45 | class ArrayType(object): 46 | json_type = "array" 47 | items = [] 48 | 49 | 50 | class ObjectType(object): 51 | json_type = "object" 52 | properties = {} 53 | 54 | 55 | class JsonSchemaTypeNotFound(Exception): 56 | pass 57 | 58 | 59 | if sys.version_info < (3,): 60 | import types 61 | 62 | SCHEMA_TYPES = { 63 | types.NoneType: NullType, 64 | types.UnicodeType: StringType, 65 | types.StringType: StringType, 66 | types.IntType: NumberType, 67 | types.FloatType: NumberType, 68 | types.LongType: NumberType, 69 | types.BooleanType: BooleanType, 70 | types.ListType: ArrayType, 71 | types.DictType: ObjectType, 72 | } 73 | else: 74 | SCHEMA_TYPES = { 75 | type(None): NullType, 76 | str: StringType, 77 | int: NumberType, 78 | float: NumberType, 79 | bool: BooleanType, 80 | list: ArrayType, 81 | dict: ObjectType, 82 | } 83 | -------------------------------------------------------------------------------- /json_schema_generator/test_template.py.tmpl: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from unittest import TestCase 6 | from json_schema_generator import Validator 7 | from six.moves.urllib.request import urlopen 8 | 9 | SERVICE_URL = "${service_url}" 10 | 11 | 12 | class TestHomogate(TestCase): 13 | 14 | def test_${homologation_name}_should_match_json_schema(self): 15 | json_data = urlopen(SERVICE_URL).read() 16 | json_schema_file_path = os.path.join("${json_schemas_dir}", "${json_schema_file_name}") 17 | validator = Validator.from_path(json_schema_file_path) 18 | is_valid = validator.assert_json(json_data) 19 | 20 | self.assertTrue(is_valid, validator.error_message) 21 | 22 | -------------------------------------------------------------------------------- /json_schema_generator/validator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | from jsonschema import validate 5 | 6 | 7 | class Validator(object): 8 | 9 | def __init__(self, json_schema_dict): 10 | self._json_schema_dict = json_schema_dict 11 | self._error_message = '' 12 | 13 | @property 14 | def json_schema_dict(self): 15 | return self._json_schema_dict 16 | 17 | @property 18 | def error_message(self): 19 | return self._error_message 20 | 21 | @classmethod 22 | def from_path(self, path): 23 | return Validator(json.load(open(path))) 24 | 25 | def assert_json(self, json_str): 26 | valid = False 27 | json_object = json.loads(json_str) 28 | 29 | try: 30 | validate(json_object, self.json_schema_dict) 31 | valid = True 32 | 33 | except Exception as e: 34 | self._error_message = "Inválido: \n\t%s" % str(e) 35 | 36 | return valid 37 | 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==2.3.0 2 | six==1.6.1 3 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import pkgutil 6 | import tests 7 | 8 | loader = unittest.TestLoader() 9 | suite = unittest.TestSuite() 10 | 11 | for importer, modname, ispkg in pkgutil.iter_modules(tests.__path__): 12 | mod = __import__('tests.%s' % modname, globals(), locals(), ['*'], -1) 13 | suite.addTests(loader.loadTestsFromModule(mod)) 14 | 15 | unittest.TextTestRunner(verbosity=2).run(suite) 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='json_schema_generator', 7 | version='0.6', 8 | description='A simple json schema generator based on json resource with auto validation tools', 9 | author='Felipe Ramos Ferreira', 10 | author_email='perenecabuto@gmail.com', 11 | maintainer='Felipe Ramos Ferreira', 12 | maintainer_email='perenecabuto@gmail.com', 13 | url='https://pypi.python.org/pypi/json_schema_generator/', 14 | 15 | scripts=['bin/jsonschema_generator.py'], 16 | include_dirs=['json_schema_generator/',], 17 | packages=['json_schema_generator'], 18 | #package_data={'jsonschema_generator': ['test_template.py.tmpl']}, 19 | include_package_data=True, 20 | install_requires=[ 21 | 'jsonschema>=2.3.0' 22 | ], 23 | zip_safe=False, 24 | 25 | keywords='json_schema, jsonschema, json, generator, api, validator', 26 | classifiers=[ 27 | 'Development Status :: 4 - Beta', 28 | 'Environment :: Console', 29 | 'Intended Audience :: Developers', 30 | 'Intended Audience :: Information Technology', 31 | 'Intended Audience :: System Administrators', 32 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 33 | 'Natural Language :: English', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2.6', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Topic :: Software Development :: Code Generators', 41 | 'Topic :: Software Development :: Quality Assurance', 42 | 'Topic :: Software Development :: Testing', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock==1.0.1 2 | 3 | -r requirements.txt 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perenecabuto/json_schema_generator/a8c2572408f5dad682c0791ae896cf617f36758d/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | null_json_schema = """ 5 | { 6 | "$schema": "http://json-schema.org/draft-03/schema", 7 | "type": "null", 8 | "id": "#", 9 | "required": true 10 | } 11 | """ 12 | 13 | number_json_schema = """ 14 | { 15 | "$schema": "http://json-schema.org/draft-03/schema", 16 | "type": "number", 17 | "id": "#", 18 | "required": true 19 | } 20 | """ 21 | 22 | string_json_schema = """ 23 | { 24 | "$schema": "http://json-schema.org/draft-03/schema", 25 | "type": "string", 26 | "id": "#", 27 | "required": true 28 | } 29 | """ 30 | 31 | boolean_json_schema = """ 32 | { 33 | "$schema": "http://json-schema.org/draft-03/schema", 34 | "type": "boolean", 35 | "id": "#", 36 | "required": true 37 | } 38 | """ 39 | 40 | boolean_json_schema = """ 41 | { 42 | "$schema": "http://json-schema.org/draft-03/schema", 43 | "type": "boolean", 44 | "id": "#", 45 | "required": true 46 | } 47 | """ 48 | 49 | array_json_schema = """ 50 | { 51 | "$schema": "http://json-schema.org/draft-03/schema", 52 | "type": "array", 53 | "id": "#", 54 | "required": true 55 | } 56 | """ 57 | 58 | array_of_number_json_schema = """ 59 | { 60 | "$schema": "http://json-schema.org/draft-03/schema", 61 | "type": "array", 62 | "id": "#", 63 | "required": true, 64 | "items": { 65 | "id": "0", 66 | "type": "number", 67 | "required": true 68 | } 69 | } 70 | """ 71 | 72 | mixed_array_json_schema = """ 73 | { 74 | "$schema": "http://json-schema.org/draft-03/schema", 75 | "type": "array", 76 | "id": "#", 77 | "required": true, 78 | "items": [ 79 | { 80 | "id": "0", 81 | "type": "string", 82 | "required": true 83 | }, 84 | { 85 | "id": "1", 86 | "type": "number", 87 | "required": true 88 | }, 89 | { 90 | "id": "2", 91 | "type": "object", 92 | "required": true 93 | } 94 | ] 95 | } 96 | """ 97 | 98 | object_json_schema = """ 99 | { 100 | "$schema": "http://json-schema.org/draft-03/schema", 101 | "type": "object", 102 | "id": "#", 103 | "required": true 104 | } 105 | """ 106 | 107 | object_with_properties_schema = """ 108 | { 109 | "$schema": "http://json-schema.org/draft-03/schema", 110 | "type": "object", 111 | "id": "#", 112 | "required": true, 113 | "properties": { 114 | "p1": { 115 | "id": "p1", 116 | "type": "number", 117 | "required": true 118 | }, 119 | "p2": { 120 | "id": "p2", 121 | "type": "string", 122 | "required": true 123 | }, 124 | "p3": { 125 | "id": "p3", 126 | "type": "boolean", 127 | "required": true 128 | } 129 | } 130 | } 131 | """ 132 | 133 | json_1 = """ 134 | { 135 | "item_1": "string_value_1", 136 | "item_2": 123, 137 | "item_3": [1, 2, 3], 138 | "item_4": true, 139 | "item_5": null, 140 | "item_6": {"key": "value"}, 141 | "item_7": { 142 | "item_7.1": "string_value_1", 143 | "item_7.2": 123, 144 | "item_7.3": [1, 2, 3], 145 | "item_7.4": true, 146 | "item_7.5": null, 147 | "item_7.6": {"key": "value"} 148 | } 149 | } 150 | """ 151 | 152 | json_schema_1 = """ 153 | { 154 | "type": "object", 155 | "$schema": "http://json-schema.org/draft-03/schema", 156 | "id": "#", 157 | "required": true, 158 | "properties": { 159 | "item_1": { 160 | "type": "string", 161 | "id": "item_1", 162 | "required": true 163 | }, 164 | "item_2": { 165 | "type": "number", 166 | "id": "item_2", 167 | "required": true 168 | }, 169 | "item_3": { 170 | "type": "array", 171 | "id": "item_3", 172 | "required": true, 173 | "items": { 174 | "type": "number", 175 | "id": "0", 176 | "required": true 177 | } 178 | }, 179 | "item_4": { 180 | "type": "boolean", 181 | "id": "item_4", 182 | "required": true 183 | }, 184 | "item_5": { 185 | "type": "null", 186 | "id": "item_5", 187 | "required": true 188 | }, 189 | "item_6": { 190 | "type": "object", 191 | "id": "item_6", 192 | "required": true, 193 | "properties": { 194 | "key": { 195 | "type": "string", 196 | "id": "key", 197 | "required": true 198 | } 199 | } 200 | }, 201 | "item_7": { 202 | "type": "object", 203 | "id": "item_7", 204 | "required": true, 205 | "properties": { 206 | "item_7.1": { 207 | "type": "string", 208 | "id": "item_7.1", 209 | "required": true 210 | }, 211 | "item_7.2": { 212 | "type": "number", 213 | "id": "item_7.2", 214 | "required": true 215 | }, 216 | "item_7.3": { 217 | "type": "array", 218 | "id": "item_7.3", 219 | "required": true, 220 | "items": { 221 | "type": "number", 222 | "id": "0", 223 | "required": true 224 | } 225 | }, 226 | "item_7.4": { 227 | "type": "boolean", 228 | "id": "item_7.4", 229 | "required": true 230 | }, 231 | "item_7.5": { 232 | "type": "null", 233 | "id": "item_7.5", 234 | "required": true 235 | }, 236 | "item_7.6": { 237 | "type": "object", 238 | "id": "item_7.6", 239 | "required": true, 240 | "properties": { 241 | "key": { 242 | "type": "string", 243 | "id": "key", 244 | "required": true 245 | } 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | """ 253 | 254 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | 6 | def normalize_json(json_str): 7 | return json.dumps(json.loads(json_str), sort_keys=True) 8 | 9 | -------------------------------------------------------------------------------- /tests/test_generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json, sys 4 | from unittest import TestCase 5 | 6 | from json_schema_generator import SchemaGenerator 7 | 8 | from .helpers import normalize_json 9 | from . import fixtures 10 | 11 | 12 | class TestGenerator(TestCase): 13 | if sys.version_info < (2, 7): 14 | def assertIsInstance(self, obj, *types): 15 | assert isinstance(obj, types) 16 | 17 | def assertIn(self, key, iterable): 18 | assert key in iterable 19 | 20 | def test_conversion(self): 21 | generator = SchemaGenerator.from_json(fixtures.json_1) 22 | 23 | gotten = generator.to_dict() 24 | expected = json.loads(fixtures.json_schema_1) 25 | 26 | self.assertEqual(gotten, expected) 27 | 28 | def test_instance(self): 29 | schema_dict = json.loads(fixtures.json_schema_1) 30 | generator = SchemaGenerator(schema_dict) 31 | 32 | self.assertIsInstance(generator, SchemaGenerator) 33 | self.assertEqual(generator.base_object, schema_dict) 34 | 35 | def test_base_object_from_json_should_match_the_submitted(self): 36 | schema_dict = json.loads(fixtures.json_schema_1) 37 | generator = SchemaGenerator.from_json(fixtures.json_schema_1) 38 | 39 | self.assertIsInstance(generator, SchemaGenerator) 40 | self.assertEqual(generator.base_object, schema_dict) 41 | 42 | def test_generator_should_instanciate_from_json(self): 43 | generator = SchemaGenerator.from_json(fixtures.json_1) 44 | 45 | self.assertIsInstance(generator, SchemaGenerator) 46 | 47 | def test_generator_should_convert_null(self): 48 | generator = SchemaGenerator.from_json('null') 49 | expected = json.loads(fixtures.null_json_schema) 50 | 51 | self.assertEqual(generator.to_dict(), expected) 52 | 53 | def test_generator_should_convert_number(self): 54 | generator = SchemaGenerator.from_json('1') 55 | expected = json.loads(fixtures.number_json_schema) 56 | 57 | self.assertEqual(generator.to_dict(), expected) 58 | 59 | def test_generator_should_convert_string(self): 60 | generator = SchemaGenerator.from_json('"str"') 61 | expected = json.loads(fixtures.string_json_schema) 62 | 63 | self.assertEqual(generator.to_dict(), expected) 64 | 65 | def test_generator_should_convert_boolean(self): 66 | generator = SchemaGenerator.from_json('true') 67 | expected = json.loads(fixtures.boolean_json_schema) 68 | 69 | self.assertEqual(generator.to_dict(), expected) 70 | 71 | def test_generator_should_convert_array(self): 72 | generator = SchemaGenerator.from_json('[]') 73 | expected = json.loads(fixtures.array_json_schema) 74 | 75 | self.assertEqual(generator.to_dict(), expected) 76 | 77 | def test_generator_should_convert_array_with_homogeneous_items(self): 78 | generator = SchemaGenerator.from_json('[1, 2, 3]') 79 | expected = json.loads(fixtures.array_of_number_json_schema) 80 | 81 | self.assertEqual(generator.to_dict(), expected) 82 | 83 | def test_generator_should_convert_array_with_hetereogeneous_items(self): 84 | generator = SchemaGenerator.from_json('["a", 1, {}]') 85 | expected = json.loads(fixtures.mixed_array_json_schema) 86 | 87 | self.assertEqual(generator.to_dict(), expected) 88 | 89 | def test_generator_should_convert_object(self): 90 | generator = SchemaGenerator.from_json('{}') 91 | expected = json.loads(fixtures.object_json_schema) 92 | 93 | self.assertEqual(generator.to_dict(), expected) 94 | 95 | def test_generator_should_convert_object_with_properties(self): 96 | generator = SchemaGenerator.from_json('{"p1": 1, "p2": "str", "p3": false}') 97 | expected = json.loads(fixtures.object_with_properties_schema) 98 | 99 | self.assertEqual(generator.to_dict(), expected) 100 | 101 | def test_generator_should_return_text_plain_json_schema(self): 102 | generator = SchemaGenerator.from_json('{"p1": 1, "p2": "str", "p3": false}') 103 | 104 | gotten = normalize_json(generator.to_json()) 105 | expected = normalize_json(fixtures.object_with_properties_schema) 106 | 107 | self.assertEqual(gotten, expected) 108 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import copy 4 | import json 5 | import os 6 | import unittest 7 | import jsonschema 8 | 9 | from json_schema_generator import SchemaGenerator 10 | 11 | class TestIntegration(unittest.TestCase): 12 | example_obj = { 13 | 'string' : 'string', 14 | 'number' : 1.0, 15 | 'obj' : { 16 | 'array_number' : [ 1, 2 ], 17 | 'array_obj' : [ { "string" : "string", "number" : 1 } ], 18 | }, 19 | 'array_obj' : [ { "string" : "string" } ], 20 | } 21 | 22 | def test_happy_path(self): 23 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj) 24 | jsonschema.validate(obj1, schema_dict) 25 | 26 | def test_extra_data_passes(self): 27 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj) 28 | obj1['newvalue'] = 1.0 29 | 30 | jsonschema.validate(obj1, schema_dict) 31 | 32 | def test_checks_object_types(self): 33 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj) 34 | obj1['string'] = 1.0 35 | 36 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 37 | 38 | def test_set_not_required__simple_obj(self): 39 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj, path_list = [ 40 | [ 'number', ], 41 | [ 'obj', 'array_number', ], 42 | [ 'obj', 'array_obj', 'string', ], 43 | [ 'array_obj', 'string', ], 44 | ], required = False) 45 | 46 | jsonschema.validate(obj1, schema_dict) 47 | 48 | obj1.pop('number') 49 | jsonschema.validate(obj1, schema_dict) 50 | 51 | obj1['number'] = None 52 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 53 | 54 | obj1['number'] = 'string' 55 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 56 | 57 | def test_set_not_required__nested_array(self): 58 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj, path_list = [ 59 | [ 'number', ], 60 | [ 'obj', 'array_number', ], 61 | [ 'obj', 'array_obj', 'string', ], 62 | [ 'array_obj', 'string', ], 63 | ], required = False) 64 | 65 | jsonschema.validate(obj1, schema_dict) 66 | 67 | obj1['obj'].pop('array_number') 68 | jsonschema.validate(obj1, schema_dict) 69 | 70 | obj1['obj']['array_number'] = None 71 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 72 | 73 | obj1['obj']['array_number'] = [ 'string' ] 74 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 75 | 76 | def test_set_not_required__nested_obj(self): 77 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj, path_list = [ 78 | [ 'number', ], 79 | [ 'obj', 'array_number', ], 80 | [ 'obj', 'array_obj', 'string', ], 81 | [ 'array_obj', 'string', ], 82 | ], required = False) 83 | 84 | jsonschema.validate(obj1, schema_dict) 85 | 86 | obj1['obj']['array_obj'][0].pop('string') 87 | jsonschema.validate(obj1, schema_dict) 88 | 89 | obj1['obj']['array_obj'][0]['string'] = None 90 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 91 | 92 | obj1['obj']['array_obj'][0]['string'] = [ 1.0 ] 93 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 94 | 95 | obj1['obj'].pop('array_obj') 96 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, obj1, schema_dict) 97 | 98 | def test_set_not_required_and_nullable(self): 99 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj, path_list = [ 100 | [ 'number', ], 101 | [ 'obj', 'array_number', ], 102 | [ 'obj', 'array_obj', 'string', ], 103 | [ 'array_obj', 'string', ], 104 | ], required = False, nullable = True) 105 | 106 | jsonschema.validate(obj1, schema_dict) 107 | 108 | obj1['obj']['array_obj'][0].pop('string') 109 | jsonschema.validate(obj1, schema_dict) 110 | 111 | obj1['obj']['array_obj'][0]['string'] = None 112 | jsonschema.validate(obj1, schema_dict) 113 | 114 | def test_set_default_required(self): 115 | schema_dict, obj1 = self.generate_schema_dict(self.example_obj, path_list = [ 116 | [ 'number', ], 117 | ], default_required = False, default_nullable = True, required = True, nullable = False) 118 | 119 | jsonschema.validate(obj1, schema_dict) 120 | jsonschema.validate({ 'number' : 1 }, schema_dict) 121 | jsonschema.validate({ 'number' : 1, 'string' : None }, schema_dict) 122 | 123 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, { 'number' : None }, schema_dict) 124 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, { 'number' : 'string' }, schema_dict) 125 | self.assertRaises(jsonschema.ValidationError, jsonschema.validate, { 'number' : 1, 'string' : 1 }, schema_dict) 126 | 127 | def generate_schema_dict(self, example_event, path_list = [], default_required = True, default_nullable = False, required = False, nullable = False): 128 | schema_dict = SchemaGenerator(example_event).to_dict(required = default_required, nullable = default_nullable) 129 | 130 | for path in path_list: 131 | SchemaGenerator.set_required(schema_dict, path, required = required, nullable = nullable) 132 | 133 | # Sanity check that the example event still passes validation 134 | jsonschema.validate(example_event, schema_dict) 135 | 136 | return schema_dict, copy.deepcopy(example_event) 137 | -------------------------------------------------------------------------------- /tests/test_recorder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | 6 | from unittest import TestCase 7 | from fudge import patch 8 | from . import fixtures 9 | 10 | from json_schema_generator import Recorder 11 | 12 | 13 | class TestRecorder(TestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.service_url = 'http://fake_url/with.json' 18 | cls.json_schema_file_path = os.path.join(os.path.dirname(__file__), 'tmp', 'test.json_schema') 19 | 20 | @classmethod 21 | def tearDownClass(cls): 22 | if os.path.exists(cls.json_schema_file_path): 23 | os.remove(cls.json_schema_file_path) 24 | 25 | @patch('six.moves.urllib.request.urlopen') 26 | def test_recorder_should_get_json_from_url(self, fake_urlopen=None): 27 | fake_urlopen.is_callable().expects_call().returns_fake() \ 28 | .provides('read').returns(fixtures.json_1) 29 | 30 | rec = Recorder.from_url(self.service_url) 31 | rec.save_json_schema(self.json_schema_file_path) 32 | 33 | expected = json.loads(fixtures.json_schema_1) 34 | gotten = json.load(open(self.json_schema_file_path)) 35 | 36 | self.assertEqual(gotten, expected) 37 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from unittest import TestCase 5 | 6 | from json_schema_generator.schema_types import ( 7 | Type, 8 | NullType, 9 | StringType, 10 | NumberType, 11 | BooleanType, 12 | ArrayType, 13 | ObjectType, 14 | ) 15 | 16 | 17 | class TestSchemaTypes(TestCase): 18 | if sys.version_info < (2, 7): 19 | def assertIsInstance(self, obj, *types): 20 | assert isinstance(obj, types) 21 | 22 | def assertIn(self, key, iterable): 23 | assert key in iterable 24 | 25 | 26 | def test_schema_verion(self): 27 | self.assertEqual(Type.schema_version, "http://json-schema.org/draft-03/schema") 28 | 29 | def test_integer_type(self): 30 | gotten = Type.get_schema_type_for(type(1)) 31 | 32 | self.assertEqual(gotten, NumberType) 33 | self.assertEqual(gotten.json_type, "number") 34 | 35 | def test_float_type(self): 36 | gotten = Type.get_schema_type_for(type(1.1)) 37 | 38 | self.assertEqual(gotten, NumberType) 39 | self.assertEqual(gotten.json_type, "number") 40 | 41 | if sys.version_info < (3,): 42 | def test_long_type(self): 43 | gotten = Type.get_schema_type_for(long) 44 | 45 | self.assertEqual(gotten, NumberType) 46 | self.assertEqual(gotten.json_type, "number") 47 | 48 | def test_string_type(self): 49 | gotten = Type.get_schema_type_for(type("str")) 50 | 51 | self.assertEqual(gotten, StringType) 52 | self.assertEqual(gotten.json_type, "string") 53 | 54 | def test_unicode_type(self): 55 | gotten = Type.get_schema_type_for(type(u"str")) 56 | 57 | self.assertEqual(gotten, StringType) 58 | self.assertEqual(gotten.json_type, "string") 59 | 60 | def test_null_type(self): 61 | gotten = Type.get_schema_type_for(type(None)) 62 | 63 | self.assertEqual(gotten, NullType) 64 | self.assertEqual(gotten.json_type, "null") 65 | 66 | def test_boolean_type(self): 67 | gotten = Type.get_schema_type_for(type(True)) 68 | 69 | self.assertEqual(gotten, BooleanType) 70 | self.assertEqual(gotten.json_type, "boolean") 71 | 72 | def test_array_type(self): 73 | gotten = Type.get_schema_type_for(type([])) 74 | 75 | self.assertEqual(gotten, ArrayType) 76 | self.assertEqual(gotten.json_type, "array") 77 | self.assertIn("items", dir(gotten)) 78 | 79 | def test_object_type(self): 80 | gotten = Type.get_schema_type_for(type({})) 81 | 82 | self.assertEqual(gotten, ObjectType) 83 | self.assertEqual(gotten.json_type, "object") 84 | self.assertIn("properties", dir(gotten)) 85 | 86 | -------------------------------------------------------------------------------- /tests/test_validator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | 6 | from unittest import TestCase 7 | 8 | from . import fixtures 9 | from json_schema_generator import Validator 10 | 11 | 12 | class TestValidator(TestCase): 13 | 14 | @classmethod 15 | def setUpClass(cls): 16 | cls.json_schema_file_path = os.path.join(os.path.dirname(__file__), 'tmp', 'test.json_schema') 17 | 18 | @classmethod 19 | def tearDownClass(cls): 20 | if os.path.exists(cls.json_schema_file_path): 21 | os.remove(cls.json_schema_file_path) 22 | 23 | def test_validator_should_certify_json(self): 24 | json_schema_dict = json.loads(fixtures.json_schema_1) 25 | validator = Validator(json_schema_dict) 26 | 27 | gotten = validator.assert_json(fixtures.json_1) 28 | 29 | self.assertEqual(gotten, True) 30 | 31 | def test_validator_should_certify_json_from_schema_file(self): 32 | with open(self.json_schema_file_path, 'w') as jss_file: 33 | jss_file.write(fixtures.json_schema_1) 34 | 35 | validator = Validator.from_path(self.json_schema_file_path) 36 | 37 | gotten = validator.assert_json(fixtures.json_1) 38 | 39 | self.assertEqual(gotten, True) 40 | 41 | -------------------------------------------------------------------------------- /tests/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py33, py34, pypy 3 | [testenv] 4 | deps = 5 | pytest 6 | mock==1.0.1 7 | fudge==1.0.3 8 | six==1.6.1 9 | jsonschema==2.3.0 10 | commands = py.test --basetemp={envtmpdir} [] 11 | setenv = 12 | PROJECT_ROOT = {toxinidir} 13 | [pytest] 14 | norecursedirs = .git .tox build dist 15 | --------------------------------------------------------------------------------