├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── spec ├── data │ ├── mock_schema.graphql │ └── mock_schema.json ├── support │ └── jasmine.json └── transformSpec.js └── transform.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | package-lock.json 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - "node_modules" 5 | node_js: 6 | - "7" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jakub Fiala 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-json-schema 2 | [![](https://travis-ci.org/jakubfiala/graphql-json-schema.svg?branch=master)](https://travis-ci.org/jakubfiala/graphql-json-schema) 3 | 4 | Converts GraphQL Schema Language to JSON Schema 5 | 6 | ## Installation 7 | 8 | ```shell 9 | npm install graphql-json-schema 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```js 15 | const transform = require('graphql-json-schema'); 16 | 17 | const schema = transform(` 18 | scalar Foo 19 | 20 | union MyUnion = Foo | String | Float 21 | 22 | enum MyEnum { 23 | FIRST_ITEM 24 | SECOND_ITEM 25 | THIRD_ITEM 26 | } 27 | 28 | type Stuff { 29 | my_field: Int 30 | req_field: String! 31 | recursion: MoreStuff 32 | custom_scalar: Foo 33 | enum: MyEnum 34 | } 35 | 36 | type MoreStuff { 37 | first: [Float] 38 | identifier: [ID]! 39 | reference: Stuff! 40 | bool: Boolean! 41 | union: MyUnion 42 | with_params(param1: Int, param2: [Float]): Int 43 | } 44 | 45 | input InputType { 46 | an_int: Int! 47 | a_string: String 48 | } 49 | `); 50 | 51 | console.log(schema); 52 | ``` 53 | 54 | the code above prints the following JSON as a plain JS object: 55 | 56 | ```json 57 | { 58 | "$schema": "http://json-schema.org/draft-04/schema#", 59 | "definitions": { 60 | "Foo": { 61 | "title": "Foo", 62 | "type": "GRAPHQL_SCALAR" 63 | }, 64 | "MyUnion": { 65 | "title": "MyUnion", 66 | "type": "GRAPHQL_UNION", 67 | "oneOf": [ 68 | { 69 | "$ref": "#/definitions/Foo" 70 | }, 71 | { 72 | "type": "string", 73 | "required": false 74 | }, 75 | { 76 | "type": "number", 77 | "required": false 78 | } 79 | ] 80 | }, 81 | "MyEnum": { 82 | "title": "MyEnum", 83 | "type": "GRAPHQL_ENUM", 84 | "enum": [ 85 | "FIRST_ITEM", 86 | "SECOND_ITEM", 87 | "THIRD_ITEM" 88 | ] 89 | }, 90 | "Stuff": { 91 | "title": "Stuff", 92 | "type": "object", 93 | "properties": { 94 | "my_field": { 95 | "type": "integer", 96 | "required": false, 97 | "title": "my_field", 98 | "arguments": [] 99 | }, 100 | "req_field": { 101 | "type": "string", 102 | "required": true, 103 | "title": "req_field", 104 | "arguments": [] 105 | }, 106 | "recursion": { 107 | "allOf": [ 108 | { 109 | "$ref": "#/definitions/MoreStuff" 110 | }, 111 | { 112 | "title": "recursion" 113 | } 114 | ] 115 | }, 116 | "custom_scalar": { 117 | "allOf": [ 118 | { 119 | "$ref": "#/definitions/Foo" 120 | }, 121 | { 122 | "title": "custom_scalar" 123 | } 124 | ] 125 | }, 126 | "enum": { 127 | "allOf": [ 128 | { 129 | "$ref": "#/definitions/MyEnum" 130 | }, 131 | { 132 | "title": "enum" 133 | } 134 | ] 135 | } 136 | }, 137 | "required": [ 138 | "req_field" 139 | ] 140 | }, 141 | "MoreStuff": { 142 | "title": "MoreStuff", 143 | "type": "object", 144 | "properties": { 145 | "first": { 146 | "type": "array", 147 | "items": { 148 | "type": { 149 | "type": "number", 150 | "required": false 151 | } 152 | }, 153 | "title": "first", 154 | "arguments": [] 155 | }, 156 | "identifier": { 157 | "type": "array", 158 | "items": { 159 | "type": { 160 | "type": "string", 161 | "required": false 162 | } 163 | }, 164 | "required": true, 165 | "title": "identifier", 166 | "arguments": [] 167 | }, 168 | "reference": { 169 | "allOf": [ 170 | { 171 | "$ref": "#/definitions/Stuff", 172 | "required": true 173 | }, 174 | { 175 | "title": "reference" 176 | } 177 | ] 178 | }, 179 | "bool": { 180 | "type": "boolean", 181 | "required": true, 182 | "title": "bool", 183 | "arguments": [] 184 | }, 185 | "union": { 186 | "allOf": [ 187 | { 188 | "$ref": "#/definitions/MyUnion" 189 | }, 190 | { 191 | "title": "union" 192 | } 193 | ] 194 | }, 195 | "with_params": { 196 | "type": "integer", 197 | "required": false, 198 | "title": "with_params", 199 | "arguments": [ 200 | { 201 | "title": "param1", 202 | "type": { 203 | "type": "integer", 204 | "required": false 205 | }, 206 | "defaultValue": null 207 | }, 208 | { 209 | "title": "param2", 210 | "type": { 211 | "type": "array", 212 | "items": { 213 | "type": { 214 | "type": "number", 215 | "required": false 216 | } 217 | } 218 | }, 219 | "defaultValue": null 220 | } 221 | ] 222 | } 223 | }, 224 | "required": [ 225 | "identifier", 226 | "bool" 227 | ] 228 | }, 229 | "InputType": { 230 | "title": "InputType", 231 | "type": "object", 232 | "input": true, 233 | "properties": { 234 | "an_int": { 235 | "type": "integer", 236 | "required": true, 237 | "title": "an_int" 238 | }, 239 | "a_string": { 240 | "type": "string", 241 | "required": false, 242 | "title": "a_string" 243 | } 244 | }, 245 | "required": [ 246 | "an_int" 247 | ] 248 | } 249 | } 250 | } 251 | ``` 252 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const parse = require('graphql/language').parse; 2 | const transform = require('./transform.js'); 3 | 4 | module.exports = schema => { 5 | if (typeof schema !== 'string') throw new TypeError('GraphQL Schema must be a string'); 6 | return transform(parse(schema)); 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-json-schema", 3 | "version": "0.1.2", 4 | "description": "Converts between GraphQL Schema Language and JSON Schema", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jasmine" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jakubfiala/graphql-json-schema.git" 12 | }, 13 | "keywords": [ 14 | "graphql", 15 | "json-schema" 16 | ], 17 | "author": "Jakub Fiala", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/jakubfiala/graphql-json-schema/issues" 21 | }, 22 | "homepage": "https://github.com/jakubfiala/graphql-json-schema#readme", 23 | "dependencies": { 24 | "graphql": "^0.10.1" 25 | }, 26 | "devDependencies": { 27 | "jasmine": "^2.6.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/data/mock_schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Foo 2 | 3 | union MyUnion = Foo | String | Float 4 | 5 | enum MyEnum { 6 | FIRST_ITEM 7 | SECOND_ITEM 8 | THIRD_ITEM 9 | } 10 | 11 | type Stuff { 12 | my_field: Int 13 | req_field: String! 14 | recursion: MoreStuff 15 | custom_scalar: Foo 16 | enum: MyEnum 17 | } 18 | 19 | type MoreStuff { 20 | first: [Float] 21 | identifier: [ID]! 22 | reference: Stuff! 23 | bool: Boolean! 24 | union: MyUnion 25 | with_params(param1: Int, param2: [Float]): Int 26 | } 27 | 28 | input InputType { 29 | an_int: Int! 30 | a_string: String 31 | } -------------------------------------------------------------------------------- /spec/data/mock_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "Foo": { 5 | "title": "Foo", 6 | "type": "GRAPHQL_SCALAR" 7 | }, 8 | "MyUnion": { 9 | "title": "MyUnion", 10 | "type": "GRAPHQL_UNION", 11 | "oneOf": [ 12 | { 13 | "$ref": "#/definitions/Foo" 14 | }, 15 | { 16 | "type": "string", 17 | "required": false 18 | }, 19 | { 20 | "type": "number", 21 | "required": false 22 | } 23 | ] 24 | }, 25 | "MyEnum": { 26 | "title": "MyEnum", 27 | "type": "GRAPHQL_ENUM", 28 | "enum": [ 29 | "FIRST_ITEM", 30 | "SECOND_ITEM", 31 | "THIRD_ITEM" 32 | ] 33 | }, 34 | "Stuff": { 35 | "title": "Stuff", 36 | "type": "object", 37 | "properties": { 38 | "my_field": { 39 | "type": "integer", 40 | "required": false, 41 | "title": "my_field", 42 | "arguments": [] 43 | }, 44 | "req_field": { 45 | "type": "string", 46 | "required": true, 47 | "title": "req_field", 48 | "arguments": [] 49 | }, 50 | "recursion": { 51 | "allOf": [ 52 | { 53 | "$ref": "#/definitions/MoreStuff" 54 | }, 55 | { 56 | "title": "recursion" 57 | } 58 | ] 59 | }, 60 | "custom_scalar": { 61 | "allOf": [ 62 | { 63 | "$ref": "#/definitions/Foo" 64 | }, 65 | { 66 | "title": "custom_scalar" 67 | } 68 | ] 69 | }, 70 | "enum": { 71 | "allOf": [ 72 | { 73 | "$ref": "#/definitions/MyEnum" 74 | }, 75 | { 76 | "title": "enum" 77 | } 78 | ] 79 | } 80 | }, 81 | "required": [ 82 | "req_field" 83 | ] 84 | }, 85 | "MoreStuff": { 86 | "title": "MoreStuff", 87 | "type": "object", 88 | "properties": { 89 | "first": { 90 | "type": "array", 91 | "items": { 92 | "type": { 93 | "type": "number", 94 | "required": false 95 | } 96 | }, 97 | "title": "first", 98 | "arguments": [] 99 | }, 100 | "identifier": { 101 | "type": "array", 102 | "items": { 103 | "type": { 104 | "type": "string", 105 | "required": false 106 | } 107 | }, 108 | "required": true, 109 | "title": "identifier", 110 | "arguments": [] 111 | }, 112 | "reference": { 113 | "allOf": [ 114 | { 115 | "$ref": "#/definitions/Stuff", 116 | "required": true 117 | }, 118 | { 119 | "title": "reference" 120 | } 121 | ] 122 | }, 123 | "bool": { 124 | "type": "boolean", 125 | "required": true, 126 | "title": "bool", 127 | "arguments": [] 128 | }, 129 | "union": { 130 | "allOf": [ 131 | { 132 | "$ref": "#/definitions/MyUnion" 133 | }, 134 | { 135 | "title": "union" 136 | } 137 | ] 138 | }, 139 | "with_params": { 140 | "type": "integer", 141 | "required": false, 142 | "title": "with_params", 143 | "arguments": [ 144 | { 145 | "title": "param1", 146 | "type": { 147 | "type": "integer", 148 | "required": false 149 | }, 150 | "defaultValue": null 151 | }, 152 | { 153 | "title": "param2", 154 | "type": { 155 | "type": "array", 156 | "items": { 157 | "type": { 158 | "type": "number", 159 | "required": false 160 | } 161 | } 162 | }, 163 | "defaultValue": null 164 | } 165 | ] 166 | } 167 | }, 168 | "required": [ 169 | "identifier", 170 | "bool" 171 | ] 172 | }, 173 | "InputType": { 174 | "title": "InputType", 175 | "type": "object", 176 | "input": true, 177 | "properties": { 178 | "an_int": { 179 | "type": "integer", 180 | "required": true, 181 | "title": "an_int" 182 | }, 183 | "a_string": { 184 | "type": "string", 185 | "required": false, 186 | "title": "a_string" 187 | } 188 | }, 189 | "required": [ 190 | "an_int" 191 | ] 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /spec/transformSpec.js: -------------------------------------------------------------------------------- 1 | const transform = require('../index.js'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const mockJSONSchema = require(path.join(__dirname, 'data/mock_schema.json')); 6 | const mockGraphQL = fs.readFileSync(path.join(__dirname, 'data/mock_schema.graphql'), { encoding: 'utf-8' }); 7 | 8 | describe('GraphQL to JSON Schema transform', () => { 9 | it('fails if the schema is not a string', () => { 10 | expect(() => transform(Math.PI)).toThrowError(); 11 | }); 12 | 13 | it('fails if the schema is not a valid GraphQL schema', () => { 14 | expect(() => transform(` 15 | type MyBrokenType { 16 | semicolon: String; 17 | } 18 | `)).toThrowError(); 19 | }); 20 | 21 | it('parses a test GraphQL Schema properly', () => { 22 | expect(transform(mockGraphQL)).toEqual(mockJSONSchema); 23 | }); 24 | }) 25 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mapping between GQL primitive types and JSON Schema property types 3 | * 4 | * @type {} 5 | */ 6 | const PRIMITIVES = { 7 | Int: 'integer', 8 | Float: 'number', 9 | String: 'string', 10 | Boolean: 'boolean', 11 | ID: 'string' 12 | }; 13 | 14 | /** 15 | * returns a JSON schema property type for a given GQL field type 16 | * 17 | * @param {object} type The GQL type object 18 | * @return {Object} the property type object or a reference to a type definition 19 | */ 20 | const getPropertyType = type => { 21 | switch (type.kind) { 22 | case 'NonNullType': 23 | return Object.assign(getPropertyType(type.type), { required: true }); 24 | case 'ListType': 25 | return { 26 | type: 'array', 27 | items: { 28 | type: getPropertyType(type.type) 29 | } 30 | } 31 | default: 32 | if (type.name.value in PRIMITIVES) { 33 | return { 34 | type: PRIMITIVES[type.name.value], 35 | required: false 36 | }; 37 | } 38 | else { 39 | return { $ref: `#/definitions/${type.name.value}` }; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * converts the GQL arguments array into a plain JSON schema array 46 | * 47 | * @param {Array} _arguments The GQL arguments 48 | * @return {Object} a plain JSON array 49 | */ 50 | const toFieldArguments = _arguments => { 51 | return _arguments.map(a => { 52 | return { 53 | title: a.name.value, 54 | type: getPropertyType(a.type), 55 | defaultValue: a.defaultValue 56 | }; 57 | }); 58 | } 59 | 60 | /** 61 | * maps a GQL type field onto a JSON Schema property 62 | * 63 | * @param {object} field The GQL field object 64 | * @return {Object} a plain JS object containing the property schema or a reference to another definition 65 | */ 66 | const toSchemaProperty = field => { 67 | let propertyType = getPropertyType(field.type); 68 | 69 | if ('$ref' in propertyType) propertyType = { allOf: [propertyType, { title: field.name.value }] }; 70 | 71 | return Object.assign( 72 | propertyType, 73 | { title: field.name.value }, 74 | field.arguments ? { arguments: toFieldArguments(field.arguments) } : {} 75 | ); 76 | } 77 | 78 | /** 79 | * Converts a single GQL definition into a plain JS schema object 80 | * 81 | * @param {Object} definition The GQL definition object 82 | * @return {Object} A plain JS schema object 83 | */ 84 | const toSchemaObject = definition => { 85 | if (definition.kind === 'ScalarTypeDefinition') { 86 | return { 87 | title: definition.name.value, 88 | type: 'GRAPHQL_SCALAR' 89 | } 90 | } 91 | else if (definition.kind === 'UnionTypeDefinition') { 92 | return { 93 | title: definition.name.value, 94 | type: 'GRAPHQL_UNION', 95 | oneOf: definition.types.map(getPropertyType) 96 | } 97 | } 98 | else if (definition.kind === 'EnumTypeDefinition') { 99 | return { 100 | title: definition.name.value, 101 | type: 'GRAPHQL_ENUM', 102 | enum: definition.values.map(v => v.name.value) 103 | }; 104 | } 105 | 106 | const fields = definition.fields.map(toSchemaProperty); 107 | 108 | const properties = {}; 109 | for (let f of fields) properties[f.title] = f.allOf ? { allOf: f.allOf } : f; 110 | 111 | const required = fields 112 | .filter(f => f.required) 113 | .map(f => f.title); 114 | 115 | let schemaObject = { 116 | title: definition.name.value, 117 | type: 'object', 118 | properties, 119 | required, 120 | }; 121 | 122 | if (definition.kind === 'InputObjectTypeDefinition') { 123 | Object.assign(schemaObject, { input: true }); 124 | } 125 | 126 | return schemaObject; 127 | } 128 | 129 | /** 130 | * GQL -> JSON Schema transform 131 | * 132 | * @param {Document} document The GraphQL document returned by the parse function of graphql/language 133 | * @return {object} A plain JavaScript object which conforms to JSON Schema 134 | */ 135 | const transform = document => { 136 | // ignore directives 137 | const definitions = document.definitions 138 | .filter(d => d.kind !== 'DirectiveDefinition') 139 | .map(toSchemaObject); 140 | 141 | const schema = { 142 | $schema: 'http://json-schema.org/draft-04/schema#', 143 | definitions: {} 144 | }; 145 | 146 | for (let def of definitions) { 147 | schema.definitions[def.title] = def; 148 | } 149 | 150 | return schema; 151 | }; 152 | 153 | module.exports = transform; 154 | --------------------------------------------------------------------------------