├── CHANGELOG.md ├── .npmignore ├── .gitignore ├── .editorconfig ├── .travis.yml ├── src └── index.js ├── can_publish ├── package.json ├── README.md └── test └── test.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | can_publish 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log* 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: node_js 3 | node_js: 4 | - '4.2.6' 5 | env: 6 | global: 7 | - CXX=g++-4.8 8 | - NODE_ENV=test 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import mergeWith from 'lodash.mergewith'; 3 | import isNil from 'lodash.isnil'; 4 | import isArray from 'lodash.isarray'; 5 | import uniq from 'lodash.uniq'; 6 | import isPlainObject from 'lodash.isplainobject'; 7 | 8 | export default function (jsonSchemas) { 9 | assert(isArray(jsonSchemas) && jsonSchemas.length >= 1, 'Must merge at least 1 JSON schema.'); 10 | return mergeWith({}, ...jsonSchemas, (mergedValue, newValue, key) => { 11 | if (isNil(mergedValue)) { 12 | return; 13 | } 14 | if (key === 'required') { 15 | return uniq(mergedValue.concat(newValue)); 16 | } 17 | if (isPlainObject(mergedValue)) { 18 | if (!isPlainObject(newValue)) { 19 | throw new Error(`Failed to merge schemas because "${key}" has different values.`); 20 | } 21 | return; 22 | } 23 | assert.deepEqual(mergedValue, newValue, `Failed to merge schemas because "${key}" has different values: ${JSON.stringify(mergedValue)} and ${JSON.stringify(newValue)}.`); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /can_publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | // ignore if not currently in the process of publishing 5 | var inInstall = require('in-publish/index.js').inInstall; 6 | if (inInstall()) process.exit(0); // carry on 7 | 8 | var path = require('path'); 9 | if (path.basename(__dirname) !== 'build') { 10 | console.error("Oops! Looks like you're running `npm publish` from the wrong directory."); 11 | console.error("Please build first with `npm run build` and then publish from the build directory with `npm publish build`."); 12 | console.error(""); 13 | process.exit(1); 14 | } 15 | 16 | var currentJSON = require('./package.json'); 17 | var baseJSON = require('../package.json'); 18 | if (currentJSON.version !== baseJSON.version) { 19 | console.error("You haven't yet built the current version of the package (" + baseJSON.version + ")!") 20 | console.error("Please build first with `npm run build` and then publish from the build directory with `npm publish build`."); 21 | console.error(""); 22 | process.exit(1); 23 | } 24 | 25 | // all good 26 | process.exit(0); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merge-json-schemas", 3 | "version": "1.0.0", 4 | "description": "Merge json schemas.", 5 | "main": "index.js", 6 | "author": "Good Eggs Inc.", 7 | "keywords": [ 8 | "merge", 9 | "json", 10 | "schema", 11 | "schemas" 12 | ], 13 | "contributors": [ 14 | "dannynelson " 15 | ], 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">=4" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/goodeggs/merge-json-schemas.git" 23 | }, 24 | "dependencies": { 25 | "lodash.isarray": "^4.0.0", 26 | "lodash.isnil": "^4.0.0", 27 | "lodash.isplainobject": "^4.0.6", 28 | "lodash.mergewith": "^4.6.0", 29 | "lodash.uniq": "^4.5.0" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.6.5", 33 | "babel-plugin-add-module-exports": "^0.1.2", 34 | "babel-polyfill": "^6.13.0", 35 | "babel-preset-es2015": "^6.6.0", 36 | "babel-register": "^6.7.2", 37 | "eslint": "3.1.1", 38 | "eslint-plugin-goodeggs": "^3.3.1", 39 | "eslint-plugin-lodash": "^1.10.1", 40 | "eslint-plugin-mocha": "^4.5.1", 41 | "goodeggs-test-helpers": "^1.0.0", 42 | "in-publish": "^2.0.0", 43 | "mocha": "^2.4.5" 44 | }, 45 | "babel": { 46 | "presets": [ 47 | "es2015" 48 | ], 49 | "plugins": [ 50 | "add-module-exports" 51 | ] 52 | }, 53 | "scripts": { 54 | "build": "rm -rf build/ && babel src -d build && cp package.json can_publish build/", 55 | "prepublish": "./can_publish", 56 | "postpublish": "npm cache clean", 57 | "lint": "eslint 'src/**/*.js' --ignore-path .gitignore", 58 | "test:mocha": "NODE_ENV=test mocha --compilers=js:babel-register --require=babel-polyfill 'test/test.js'", 59 | "test": "npm run lint && npm run test:mocha --" 60 | }, 61 | "publishConfig": { 62 | "registry": "https://registry.npmjs.org/", 63 | "always-auth": true 64 | }, 65 | "eslintConfig": { 66 | "plugins": [ 67 | "goodeggs" 68 | ], 69 | "extends": [ 70 | "plugin:goodeggs/goodeggs" 71 | ], 72 | "env": { 73 | "node": true 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # merge-json-schemas 2 | 3 | Intended for [consumer driven contracts](http://martinfowler.com/articles/consumerDrivenContracts.html). Define a [JSON schema](http://json-schema.org/documentation.html) for each consumer of a service, then merge those schemas into a single schema that can be used for validating the service provider. 4 | 5 | `mergeJsonSchemas` will: 6 | - merge keys in all schemas 7 | - error if keys are incompatible 8 | - create a union of "required" values 9 | 10 | ## Usage 11 | 12 | 13 | ```js 14 | import mergeJsonSchemas from 'merge-json-schemas' 15 | 16 | const consumer1Schema = { 17 | type: 'object', 18 | required: ['name'], 19 | properties: { 20 | name: { 21 | type: 'string', 22 | maxLength: 20, 23 | }, 24 | }, 25 | }; 26 | 27 | const consumer2Schema = { 28 | type: 'object', 29 | required: ['gender'], 30 | properties: { 31 | name: { 32 | type: 'string', 33 | minLength: 1, 34 | }, 35 | gender: { 36 | type: 'string', 37 | enum: ['male', 'female'], 38 | }, 39 | }, 40 | }; 41 | 42 | mergeJsonSchemas([consumer1Schema, consumer2Schema]); 43 | ``` 44 | 45 | Creates a merged schema of: 46 | ```js 47 | { 48 | type: 'object', 49 | required: ['name', 'gender'], 50 | properties: { 51 | name: { 52 | type: 'string', 53 | minLength: 1, 54 | maxLength: 20, 55 | }, 56 | gender: { 57 | type: 'string', 58 | enum: ['male', 'female'], 59 | }, 60 | }, 61 | } 62 | ``` 63 | 64 | ## mergeJsonShemas vs allOf 65 | 66 | JSON schema supports an `allOf` option, which validates against multiple schemas. 67 | ```js 68 | const providerSchema = { 69 | allOf: [consumer1Schema, consumer2Schema] 70 | }; 71 | ``` 72 | 73 | However, `mergeJsonSchemas` has a few advantages: 74 | - checks compatibility - it validates that the schemas are compatible with each other 75 | - simpler validation error messages - Validating with a library like [tv4](https://github.com/geraintluff/tv4), merged schema validation errors are easy to diagnose than `allOf` validation errors. 76 | - faster validation - it generates a smaller provider schema for faster validation 77 | - easier provider design - it provides wholistic representation of what the provider should give to the consumers 78 | - test factories - using a test factory tool like [JSON Schema Factory](https://github.com/goodeggs/unionized), you can derive factories from the provider schema for testing all of the consumers (rather than duplicating factories for each consumer). 79 | 80 | ## Contributing 81 | 82 | This module is written in ES2015 and converted to node-friendly CommonJS via 83 | [Babel](http://babeljs.io/). 84 | 85 | To compile the `src` directory to `build`: 86 | 87 | ``` 88 | npm run build 89 | ``` 90 | 91 | ## Deploying a new version 92 | 93 | ``` 94 | npm version [major|minor|patch] 95 | npm run build 96 | npm publish build # publish the build directory instead of the main directory 97 | git push --follow-tags # update github 98 | ``` 99 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env goodeggs/server-side-test */ 2 | import 'goodeggs-test-helpers'; 3 | 4 | import mergeJsonSchemas from '../src'; 5 | 6 | describe('mergeJsonSchemas', function () { 7 | it('validates at least one json schema', function () { 8 | expect(() => mergeJsonSchemas()).to.throw('Must merge at least 1 JSON schema.'); 9 | }); 10 | 11 | it('merges different properties', function () { 12 | const jsonSchema1 = { 13 | type: 'object', 14 | properties: { 15 | name: { 16 | type: 'string', 17 | }, 18 | birthDay: { 19 | type: 'string', 20 | minLength: 1, 21 | }, 22 | }, 23 | }; 24 | const jsonSchema2 = { 25 | type: 'object', 26 | properties: { 27 | birthDay: { 28 | type: 'string', 29 | format: 'date-time', 30 | }, 31 | gender: { 32 | type: 'string', 33 | enum: ['male', 'female'], 34 | }, 35 | }, 36 | }; 37 | const mergedSchema = mergeJsonSchemas([jsonSchema1, jsonSchema2]); 38 | expect(mergedSchema).to.deep.equal({ 39 | type: 'object', 40 | properties: { 41 | name: { 42 | type: 'string', 43 | }, 44 | birthDay: { 45 | type: 'string', 46 | minLength: 1, 47 | format: 'date-time', 48 | }, 49 | gender: { 50 | type: 'string', 51 | enum: ['male', 'female'], 52 | }, 53 | }, 54 | }); 55 | }); 56 | 57 | it('errors if schemas have key that is not deep equal', function () { 58 | const jsonSchema1 = { 59 | type: 'object', 60 | properties: { 61 | name: { 62 | type: ['string', 'boolean'], 63 | }, 64 | }, 65 | }; 66 | const jsonSchema2 = { 67 | type: 'object', 68 | properties: { 69 | name: { 70 | type: ['string', 'null'], 71 | }, 72 | }, 73 | }; 74 | expect( 75 | () => mergeJsonSchemas([jsonSchema1, jsonSchema2]) 76 | ).to.throw('Failed to merge schemas because "type" has different values: ["string","boolean"] and ["string","null"].'); 77 | }); 78 | 79 | it('merges if values are deep equal', function () { 80 | const jsonSchema1 = { 81 | type: 'object', 82 | properties: { 83 | name: { 84 | type: ['string', 'null'], 85 | }, 86 | }, 87 | }; 88 | const jsonSchema2 = { 89 | type: 'object', 90 | properties: { 91 | name: { 92 | type: ['string', 'null'], 93 | }, 94 | age: { 95 | type: 'number', 96 | }, 97 | }, 98 | }; 99 | const mergedSchema = mergeJsonSchemas([jsonSchema1, jsonSchema2]); 100 | expect(mergedSchema).to.deep.equal({ 101 | type: 'object', 102 | properties: { 103 | name: { 104 | type: ['string', 'null'], 105 | }, 106 | age: { 107 | type: 'number', 108 | }, 109 | }, 110 | }); 111 | }); 112 | 113 | it('combines required fields', function () { 114 | const jsonSchema1 = { 115 | type: 'object', 116 | required: ['name'], 117 | properties: { 118 | name: { 119 | type: 'string', 120 | }, 121 | birthDay: { 122 | type: 'string', 123 | minLength: 1, 124 | }, 125 | }, 126 | }; 127 | const jsonSchema2 = { 128 | type: 'object', 129 | required: ['gender'], 130 | properties: { 131 | birthDay: { 132 | type: 'string', 133 | format: 'date-time', 134 | }, 135 | gender: { 136 | type: 'string', 137 | enum: ['male', 'female'], 138 | }, 139 | }, 140 | }; 141 | const mergedSchema = mergeJsonSchemas([jsonSchema1, jsonSchema2]); 142 | expect(mergedSchema).to.deep.equal({ 143 | type: 'object', 144 | required: ['name', 'gender'], 145 | properties: { 146 | name: { 147 | type: 'string', 148 | }, 149 | birthDay: { 150 | type: 'string', 151 | minLength: 1, 152 | format: 'date-time', 153 | }, 154 | gender: { 155 | type: 'string', 156 | enum: ['male', 'female'], 157 | }, 158 | }, 159 | }); 160 | }); 161 | }); 162 | --------------------------------------------------------------------------------