├── .codacy.yaml ├── tests ├── index.spec.js └── unit │ ├── mock-sample │ ├── json.js │ ├── json-schema.js │ ├── oas2_basic.json │ ├── oas3_basic.json │ ├── oas3_ref_error.json │ ├── oas3_ref_error_flattered.json │ ├── oas3_allOf_anyOf.json │ ├── oas3_allof_oneof.json │ ├── oas3_basic_flattered.json │ ├── oas2_basic_flattered.json │ ├── oas3_allOf_anyOf_flattered.json │ └── oas3_allof_oneof_flattered.json │ └── lib │ └── core │ ├── formats.spec.js │ ├── oasFlatter.spec.js │ ├── converter-to-boolean.spec.js │ ├── types │ └── number.spec.js │ ├── converter-to-string.spec.js │ ├── schema.spec.js │ ├── default-value.spec.js │ ├── schemaCache.spec.js │ ├── converter-to-array.spec.js │ └── normalizer.spec.js ├── docs └── normalizer-schema.png ├── index.js ├── lib ├── index.js ├── core │ ├── schemaCache.js │ ├── formats.js │ ├── schema.js │ ├── types.js │ ├── oasFlatter.js │ └── normalizer.js └── helpers │ └── objectPathHelper.js ├── config ├── normalizer.js └── logger.js ├── CONTRIBUTING.md ├── SECURITY.md ├── jest.config.js ├── .circleci └── config.yml ├── LICENSE.md ├── .gitignore ├── package.json ├── CODE_OF_CONDUCT.md ├── eslint.config.mjs ├── CHANGELOG.md └── README.md /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'README.md' 4 | - 'CHANGELOG.md' 5 | -------------------------------------------------------------------------------- /tests/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('start.js', () => { 2 | it('launching the app', async() => {}); 3 | }); 4 | -------------------------------------------------------------------------------- /docs/normalizer-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjamin-allion/json-node-normalizer/HEAD/docs/normalizer-schema.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { 2 | normalize, normalizePaths, NodeTypes, oasFlatten, clearCache 3 | } = require('./lib'); 4 | 5 | module.exports = { 6 | normalize, normalizePaths, NodeTypes, oasFlatten, clearCache 7 | }; 8 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { normalize, normalizePaths, clearCache } = require('./core/normalizer'); 2 | const { NodeTypes } = require('./core/types'); 3 | const { oasFlatten } = require('./core/oasFlatter'); 4 | 5 | module.exports = { 6 | normalize, 7 | clearCache, 8 | normalizePaths, 9 | NodeTypes, 10 | oasFlatten 11 | }; 12 | -------------------------------------------------------------------------------- /config/normalizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Json normalizer default configuration 3 | * @type {object} 4 | */ 5 | const defaultConfig = { 6 | typeFieldName: 'type', 7 | formatFieldName: 'format', 8 | defaultFieldName: 'default', 9 | useCache: false, 10 | cacheId: '$id', 11 | cacheDuration: 100, 12 | excludePaths: [], 13 | }; 14 | 15 | module.exports = { 16 | defaultConfig, 17 | }; 18 | -------------------------------------------------------------------------------- /config/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | let logger = { 4 | debug: () => {} 5 | }; 6 | 7 | if (process.env.JSON_NODE_NORMALIZER_DEBUG?.toLowerCase() === 'true') { 8 | logger = createLogger({ 9 | level: process.env.JSON_NODE_NORMALIZER_LOGGING_LEVEL || 'info', 10 | format: format.json(), 11 | transports: [new transports.Console()], 12 | }); 13 | } 14 | 15 | module.exports = { 16 | logger, 17 | }; 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a fork and pull request. 5 | 6 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details when necessary. 13 | 3. You may merge the Pull Request in once you have the sign-off of one other main contributor developer, or if you 14 | do not have permission to do that, you may request a main contributor to merge it for you. 15 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/json.js: -------------------------------------------------------------------------------- 1 | // JSON Object with some fields that must be converted to Array 2 | const jsonToConvertSample = { 3 | root: { 4 | subField: { 5 | mProperty: 'TEST', 6 | mArrayProperty: { 7 | value: 'TEST', 8 | }, 9 | }, 10 | subArray: [ 11 | { 12 | mProperty: 'TEST', 13 | mArrayProperty: { 14 | value: 'TEST', 15 | }, 16 | }, 17 | { 18 | mProperty: 'TEST', 19 | mArrayProperty: { 20 | values: { 21 | value: 'TEST', 22 | }, 23 | }, 24 | }, 25 | { 26 | mProperty: 'TEST', 27 | mArrayProperty: { 28 | values: [ 29 | { 30 | value: 'TEST', 31 | }, 32 | { 33 | value: 'TEST', 34 | }, 35 | ], 36 | }, 37 | }, 38 | ], 39 | }, 40 | }; 41 | 42 | module.exports = jsonToConvertSample; 43 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 16 | 17 | ## Reporting a Vulnerability 18 | 19 | 26 | 27 | To report a security issue, please email ```benjamin.allion.pro@gmail.com``` with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. 28 | 29 | This project follows a 90 day disclosure timeline. 30 | -------------------------------------------------------------------------------- /lib/core/schemaCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * * 3 | * Class used to store schema information (dereferenced schema for ex.) 4 | */ 5 | class SchemaCache { 6 | /** 7 | * Default constructor 8 | */ 9 | constructor() { 10 | this.cache = {}; 11 | } 12 | 13 | /** 14 | * Returns data object from cache 15 | * @param {string} objectName 16 | * @return {object} 17 | */ 18 | getData(objectName) { 19 | return this.cache[objectName]; 20 | } 21 | 22 | /** 23 | * Set data into the cache 24 | * @param {string} objectName 25 | * @param {object} objectValue 26 | * @param {number} cacheDuration 27 | */ 28 | setData(objectName, objectValue, cacheDuration) { 29 | if (this.cache[objectName]) { return; } 30 | this.cache[objectName] = objectValue; 31 | setTimeout(() => { delete this.cache[objectName]; }, Number(cacheDuration)); 32 | } 33 | 34 | /** 35 | * Reset cache 36 | */ 37 | clearCache() { 38 | this.cache = {}; 39 | } 40 | } 41 | 42 | module.exports = new SchemaCache(); 43 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | coverageDirectory: 'coverage', 4 | coverageThreshold: { 5 | global: { 6 | branches: 98, 7 | functions: 98, 8 | lines: 98, 9 | statements: 98, 10 | }, 11 | './lib/**/*.js': { 12 | branches: 98, 13 | functions: 98, 14 | lines: 98, 15 | statements: 98, 16 | }, 17 | './lib/*.js': { 18 | branches: 98, 19 | functions: 98, 20 | lines: 98, 21 | statements: 98, 22 | }, 23 | }, 24 | moduleFileExtensions: [ 25 | 'js', 26 | 'json', 27 | 'node', 28 | ], 29 | moduleDirectories: ['node_modules'], 30 | moduleNameMapper: { '^@/(.*)$': '/lib/$1' }, 31 | collectCoverageFrom: [ 32 | 'lib/**/*.js', 33 | 'lib/*.js', 34 | ], 35 | testEnvironment: 'node', 36 | 37 | 38 | testMatch: [ 39 | // '**/tests/**/*.[jt]s?(x)', 40 | '**/?(*.)+(spec|test).[tj]s?(x)', 41 | ], 42 | 43 | testPathIgnorePatterns: [ 44 | '/node_modules/', 45 | ], 46 | 47 | watchman: true, 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: node:22.6.0-alpine 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | # Download and cache dependencies 13 | - restore_cache: 14 | key: v1-dependencies-{{ checksum "yarn.lock" }} 15 | 16 | - run: yarn install 17 | 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "yarn.lock" }} 22 | 23 | test: 24 | docker: 25 | - image: node:22.6.0-alpine 26 | steps: 27 | - checkout 28 | - restore_cache: 29 | key: v1-dependencies-{{ checksum "yarn.lock" }} 30 | 31 | - run: yarn install 32 | - run: yarn test # test 33 | - run: yarn coverage:codacy # check coverage 34 | - run: yarn lint # lint 35 | 36 | workflows: 37 | version: 2 38 | build_and_test: 39 | jobs: 40 | - build 41 | - test: 42 | requires: 43 | - build 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Rayed Benbrahim & Benjamin Allion 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 | -------------------------------------------------------------------------------- /.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 (https://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 | # next.js build output 61 | .next 62 | 63 | \.idea/ 64 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/json-schema.js: -------------------------------------------------------------------------------- 1 | const schemaWithRef = { 2 | $id: 'https://example.com/arrays.schema.json', 3 | $schema: 'http://json-schema.org/draft-07/schema#', 4 | description: 'A representation of a person, company, organization, or place', 5 | type: 'object', 6 | properties: { 7 | fruits: { 8 | type: 'array', 9 | items: { 10 | type: 'string', 11 | }, 12 | }, 13 | vegetables: { 14 | type: 'array', 15 | items: { $ref: '#/definitions/veggie' }, 16 | }, 17 | other: { 18 | type: 'object', 19 | }, 20 | dateOfPurchase: { 21 | type: 'string', 22 | format: 'date-time', 23 | }, 24 | }, 25 | definitions: { 26 | veggie: { 27 | type: 'object', 28 | required: ['veggieName', 'veggieLike'], 29 | properties: { 30 | veggieName: { 31 | type: 'string', 32 | description: 'The name of the vegetable.', 33 | }, 34 | veggieLike: { 35 | type: 'boolean', 36 | description: 'Do I like this vegetable?', 37 | }, 38 | veggieColor: { 39 | type: 'string', 40 | description: 'Color of the vegetable', 41 | }, 42 | }, 43 | }, 44 | }, 45 | }; 46 | 47 | module.exports = { 48 | schemaWithRef, 49 | }; 50 | -------------------------------------------------------------------------------- /lib/core/formats.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('../../config/logger'); 2 | 3 | const FormatTypes = { 4 | UPPERCASE: 'uppercase', 5 | LOWERCASE: 'lowercase', 6 | }; 7 | 8 | /** 9 | * Method that normalize json object format. 10 | * Ex: - Replace json string value to uppercase ('UPPERCASE') 11 | * See 'FormatTypes'. 12 | * @param jsonNode - json node to normalize 13 | * @param format - target format 14 | */ 15 | const normalizeFormat = (jsonNode, format) => { 16 | logger.debug(`Normalize format of ${JSON.stringify(jsonNode)} to '${format}'`); 17 | const type = typeof jsonNode; 18 | 19 | switch (type) { 20 | case 'string': 21 | return _normalizeStringFormat(jsonNode, format); 22 | default: 23 | } 24 | return jsonNode; 25 | }; 26 | 27 | /** 28 | * Method that normalize json string. 29 | * Ex: - Replace json string value to uppercase ('UPPERCASE') 30 | * See 'FormatTypes'. 31 | * @param jsonValue - json string to normalize 32 | * @param format - target format 33 | * @private 34 | */ 35 | const _normalizeStringFormat = (jsonValue, format) => { 36 | switch (format) { 37 | case FormatTypes.UPPERCASE: 38 | return jsonValue.toUpperCase(); 39 | case FormatTypes.LOWERCASE: 40 | return jsonValue.toLowerCase(); 41 | default: 42 | } 43 | return jsonValue; 44 | }; 45 | 46 | module.exports = { 47 | FormatTypes, 48 | normalizeFormat 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-node-normalizer", 3 | "version": "1.0.14-wip", 4 | "description": "NodeJS module that normalize json data types from json schema specifications.", 5 | "contributors": [ 6 | { 7 | "name": "json-node-normalizer contributors", 8 | "url": "https://github.com/benjamin-allion/json-type-converter/graphs/contributors" 9 | } 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/benjamin-allion/json-node-normalizer.git" 14 | }, 15 | "main": "index.js", 16 | "scripts": { 17 | "test": "cross-env JSON_NODE_NORMALIZER_DEBUG=true JSON_NODE_NORMALIZER_LOGGING_LEVEL=debug jest", 18 | "lint": "eslint \"./lib/**/*.js\" \"./config/**/*.js\" \"./tests/**/*.js\"", 19 | "lint:fix": "eslint --fix \"./lib/**/*.js\" \"./config/**/*.js\" \"./tests/**/*.js\"", 20 | "coverage": "cross-env JSON_NODE_NORMALIZER_LOGGING_LEVEL=debug jest --coverage", 21 | "coverage:codacy": "yarn coverage && cat ./coverage/lcov.info | codacy-coverage" 22 | }, 23 | "keywords": [ 24 | "json", 25 | "transform", 26 | "node", 27 | "type", 28 | "converter", 29 | "normalizer" 30 | ], 31 | "author": "ALLION Benjamin, BENBRAHIM Rayed", 32 | "license": "MIT", 33 | "dependencies": { 34 | "json-schema-deref": "^0.5.0", 35 | "jsonpath": "^1.1.1", 36 | "lodash": "^4.17.21", 37 | "winston": "^3.14.2" 38 | }, 39 | "devDependencies": { 40 | "globals": "^15.9.0", 41 | "eslint": "9.9.0", 42 | "@eslint/js": "9.9.0", 43 | "jest": "^29.7.0", 44 | "codacy-coverage": "^3.4.0", 45 | "cross-env": "7.0.3" 46 | }, 47 | "engines": { 48 | "node": ">=22.6.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas2_basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "schema": { 14 | "$ref": "#/definitions/Animals" 15 | } 16 | } 17 | } 18 | } 19 | }, 20 | "/pets/:id": { 21 | "get": { 22 | "responses": { 23 | "200": { 24 | "description": "1 pet", 25 | "schema": { 26 | "$ref": "#/definitions/Animal" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | "definitions": { 34 | "Animals": { 35 | "type": "array", 36 | "items": { 37 | "$ref": "#/definitions/Animal" 38 | } 39 | }, 40 | "Animal": { 41 | "type": "object", 42 | "required": [ 43 | "race", 44 | "id" 45 | ], 46 | "properties": { 47 | "race": { 48 | "type": "string" 49 | }, 50 | "id": { 51 | "type": "integer" 52 | }, 53 | "name": { 54 | "type": "string" 55 | }, 56 | "isKnown": { 57 | "type": "boolean" 58 | }, 59 | "measurement": { 60 | "$ref": "#/definitions/measurement" 61 | } 62 | } 63 | }, 64 | "measurement": { 65 | "type": "object", 66 | "properties": { 67 | "height": { 68 | "type": "integer" 69 | }, 70 | "weight": { 71 | "type": "integer" 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /tests/unit/lib/core/formats.spec.js: -------------------------------------------------------------------------------- 1 | const { FormatTypes, normalizeFormat } = require('../../../../lib/core/formats'); 2 | 3 | class UnknownType { 4 | constructor(value) { 5 | this.value = value; 6 | } 7 | } 8 | 9 | describe('formats.js', () => { 10 | it('try to normalize json node to \'UPPERCASE\'', async() => { 11 | // Given 12 | const jsonNode = 'test'; 13 | const targetFormat = FormatTypes.UPPERCASE; 14 | 15 | // When 16 | const normalizedNode = normalizeFormat(jsonNode, targetFormat); 17 | 18 | // Then 19 | const expectedResult = 'TEST'; 20 | expect(normalizedNode).toBe(expectedResult); 21 | }); 22 | 23 | it('try to normalize json node to \'LOWERCASE\'', async() => { 24 | // Given 25 | const jsonNode = 'TEST'; 26 | const targetFormat = FormatTypes.LOWERCASE; 27 | 28 | // When 29 | const normalizedNode = normalizeFormat(jsonNode, targetFormat); 30 | 31 | // Then 32 | const expectedResult = 'test'; 33 | expect(normalizedNode).toBe(expectedResult); 34 | }); 35 | 36 | it('try to normalize json node to unknown format', async() => { 37 | // Given 38 | const jsonNode = 'TEST'; 39 | const targetFormat = 'unknown'; 40 | 41 | // When 42 | const normalizedNode = normalizeFormat(jsonNode, targetFormat); 43 | 44 | // Then 45 | expect(normalizedNode).toBe(jsonNode); 46 | }); 47 | 48 | it('try to normalize unknown json type object that cannot be formatted', async() => { 49 | // Given 50 | const jsonNode = new UnknownType('TEST'); 51 | const targetFormat = FormatTypes.LOWERCASE; 52 | 53 | // When 54 | const normalizedNode = normalizeFormat(jsonNode, targetFormat); 55 | 56 | // Then 57 | expect(typeof normalizedNode).toBe('object'); 58 | expect(normalizedNode).toBe(jsonNode); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /lib/helpers/objectPathHelper.js: -------------------------------------------------------------------------------- 1 | const { get: _get } = require('lodash'); 2 | 3 | /** 4 | * Expand the arrays path from the input paths, according to the input object 5 | * 6 | * @param {object} params 7 | * @param {object} params.object the source object from which retrieve the paths 8 | * @param {string[]} params.paths the original paths, not expanded 9 | * @param {string} params.arraySymbol the symbol used to define an array field in the path 10 | * @return {string[]} the input paths, expanded 11 | * 12 | * @example 13 | * // returns ['foo.bar[0].gee', 'foo.bar[1].gee'] 14 | * expandArraysPaths( 15 | * { object: { foo: { bar: [{ gee: 'valX' }, { gee: 'valY' }] } }, paths: ['foo.bar[].gee'] } 16 | * ) 17 | * @example 18 | * // returns ['foo.bar[0].gee', 'foo.bar[1].gee'] 19 | * expandArraysPaths( 20 | * { 21 | * object: { foo: { bar: [{ gee: 'valX' }, { gee: 'valY' }] } }, 22 | * paths: ['foo.bar.*.gee'], 23 | * arraySymbol: '.*' 24 | * } 25 | * ) 26 | */ 27 | const expandArraysPaths = ({ object, paths, arraySymbol = '[*]' }) => { 28 | const expandedArraysPaths = []; 29 | 30 | for (let pathIdx = 0; pathIdx < paths.length; pathIdx += 1) { 31 | const initialPath = paths[pathIdx]; 32 | const [leftPart] = initialPath.split(arraySymbol); 33 | 34 | if (leftPart === initialPath) { 35 | expandedArraysPaths.push(initialPath); 36 | continue; 37 | } 38 | const arrayContent = _get(object, leftPart); 39 | const arrayLength = arrayContent ? arrayContent.length : 0; 40 | for (let elementIdx = 0; elementIdx < arrayLength; elementIdx += 1) { 41 | const expandedPath = initialPath.replace(`${leftPart}${arraySymbol}`, `${leftPart}[${elementIdx}]`); 42 | expandedArraysPaths.push( 43 | ...expandArraysPaths({ object, paths: [expandedPath], arraySymbol }) 44 | ); 45 | } 46 | } 47 | 48 | return expandedArraysPaths; 49 | }; 50 | 51 | module.exports = { 52 | expandArraysPaths, 53 | }; 54 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/components/schemas/Animal" 19 | }, 20 | "discriminator": { 21 | "propertyName": "pet_type" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/pets/:id": { 31 | "get": { 32 | "responses": { 33 | "200": { 34 | "description": "1 pet", 35 | "content": { 36 | "application/json": { 37 | "schema": { 38 | "$ref": "#/components/schemas/Animal" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "components": { 48 | "schemas": { 49 | "Animal": { 50 | "type": "object", 51 | "required": [ 52 | "race", 53 | "id" 54 | ], 55 | "properties": { 56 | "race": { 57 | "type": "string" 58 | }, 59 | "id": { 60 | "type": "integer" 61 | }, 62 | "name": { 63 | "type": "string" 64 | }, 65 | "isKnown": { 66 | "type": "boolean" 67 | }, 68 | "measurement": { 69 | "$ref": "#/components/schemas/measurement" 70 | } 71 | } 72 | }, 73 | "measurement": { 74 | "type": "object", 75 | "properties": { 76 | "height": { 77 | "type": "integer" 78 | }, 79 | "weight": { 80 | "type": "integer" 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /tests/unit/lib/core/oasFlatter.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | 3 | describe('oasFlatter.js', () => { 4 | it('try to convert oas2 basic example', () => { 5 | // Given 6 | const jsonSample = require('../../mock-sample/oas2_basic.json'); 7 | const expected = require('../../mock-sample/oas2_basic_flattered.json'); 8 | // When 9 | const oasFlattered = JsonNodeNormalizer.oasFlatten(jsonSample); 10 | // Then 11 | expect(oasFlattered).toEqual(expected); 12 | }); 13 | 14 | it('try to convert oas3 basic example', () => { 15 | // Given 16 | const jsonSample = require('../../mock-sample/oas3_basic.json'); 17 | const expected = require('../../mock-sample/oas3_basic_flattered.json'); 18 | // When 19 | const oasFlattered = JsonNodeNormalizer.oasFlatten(jsonSample); 20 | // Then 21 | expect(oasFlattered).toEqual(expected); 22 | }); 23 | 24 | it('try to convert oas3 with ref error example', () => { 25 | // Given 26 | const jsonSample = require('../../mock-sample/oas3_ref_error.json'); 27 | const expected = require('../../mock-sample/oas3_ref_error_flattered.json'); 28 | // When 29 | const oasFlattered = JsonNodeNormalizer.oasFlatten(jsonSample); 30 | // Then 31 | expect(oasFlattered).toEqual(expected); 32 | }); 33 | 34 | it('try to convert oas3 with allOf & oneOf example', () => { 35 | // Given 36 | const jsonSample = require('../../mock-sample/oas3_allof_oneof.json'); 37 | const expected = require('../../mock-sample/oas3_allof_oneof_flattered.json'); 38 | // When 39 | const oasFlattered = JsonNodeNormalizer.oasFlatten(jsonSample); 40 | // Then 41 | expect(oasFlattered).toEqual(expected); 42 | }); 43 | 44 | it('try to convert oas3 with allOf & anyOf example', () => { 45 | // Given 46 | const jsonSample = require('../../mock-sample/oas3_allOf_anyOf.json'); 47 | const expected = require('../../mock-sample/oas3_allOf_anyOf_flattered.json'); 48 | // When 49 | const oasFlattered = JsonNodeNormalizer.oasFlatten(jsonSample); 50 | // Then 51 | expect(oasFlattered).toEqual(expected); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_ref_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/components/schemas/Pet" 19 | }, 20 | "discriminator": { 21 | "propertyName": "pet_type" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/pets/:id": { 31 | "get": { 32 | "responses": { 33 | "200": { 34 | "description": "1 pet", 35 | "content": { 36 | "application/json": { 37 | "schema": { 38 | "$ref": "#/components/schemas/Pet" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "components": { 48 | "schemas": { 49 | "Pet": { 50 | "anyOf": [ 51 | { 52 | "$ref": "#/components/schemas/Dog" 53 | }, 54 | { 55 | "type": "object", 56 | "properties": { 57 | "id": { 58 | "type": "integer" 59 | }, 60 | "name": { 61 | "type": "string" 62 | }, 63 | "isKnown": { 64 | "type": "boolean" 65 | } 66 | }, 67 | "required": [ 68 | "id" 69 | ] 70 | }, 71 | { 72 | "type": "object", 73 | "properties": { 74 | "id": { 75 | "type": "integer" 76 | }, 77 | "isKnown": { 78 | "type": "boolean" 79 | } 80 | }, 81 | "required": [ 82 | "id" 83 | ] 84 | } 85 | ] 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_ref_error_flattered.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "id": { 21 | "type": "integer" 22 | }, 23 | "name": { 24 | "type": "string" 25 | }, 26 | "isKnown": { 27 | "type": "boolean" 28 | } 29 | }, 30 | "required": [ 31 | "id" 32 | ] 33 | }, 34 | "discriminator": { 35 | "propertyName": "pet_type" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | "/pets/:id": { 45 | "get": { 46 | "responses": { 47 | "200": { 48 | "description": "1 pet", 49 | "content": { 50 | "application/json": { 51 | "schema": { 52 | "type": "object", 53 | "properties": { 54 | "id": { 55 | "type": "integer" 56 | }, 57 | "name": { 58 | "type": "string" 59 | }, 60 | "isKnown": { 61 | "type": "boolean" 62 | } 63 | }, 64 | "required": [ 65 | "id" 66 | ] 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | "components": { 76 | "schemas": { 77 | "Pet": { 78 | "type": "object", 79 | "properties": { 80 | "id": { 81 | "type": "integer" 82 | }, 83 | "name": { 84 | "type": "string" 85 | }, 86 | "isKnown": { 87 | "type": "boolean" 88 | } 89 | }, 90 | "required": [ 91 | "id" 92 | ] 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /lib/core/schema.js: -------------------------------------------------------------------------------- 1 | const jp = require('jsonpath'); 2 | const deRef = require('json-schema-deref'); 3 | const { logger } = require('../../config/logger'); 4 | const { defaultConfig } = require('../../config/normalizer'); 5 | 6 | /** 7 | * Method that extract each field path witch has a type different than 'object'. 8 | * @param {object} schema - json-schema 9 | * @param customConfig - normalization configuration parameters 10 | * @private 11 | * @returns {array} paths - array of json-path 12 | */ 13 | const _getFieldPaths = (schema, customConfig = {}) => { 14 | const config = { ...defaultConfig, ...customConfig }; 15 | let paths = jp.paths(schema, `$..[?(@.${config.typeFieldName} && @.${config.typeFieldName}!="object")]`); 16 | 17 | // Array path ['$','properties','field'] to jsonPath '$.properties.field' 18 | paths = paths.map(path => jp.stringify(path)); 19 | // Remove 'definitions' fields 20 | paths = paths.filter(path => !path.includes('.definitions.')); 21 | 22 | logger.debug(`Paths that must be normalized ${JSON.stringify(paths)}`); 23 | return paths; 24 | }; 25 | 26 | /** 27 | * Method that extract each fields that must be normalized. 28 | * @param {object} schema - json-schema 29 | * @param customConfig - normalization configuration parameters 30 | * @returns {object} map - object that contain json field 'path' + 'type' 31 | */ 32 | const getFieldsToNormalize = (schema, customConfig = {}) => { 33 | const config = { ...defaultConfig, ...customConfig }; 34 | const paths = _getFieldPaths(schema, config); 35 | const fields = []; 36 | paths.forEach(path => { 37 | const jpValue = jp.value(schema, path); 38 | 39 | const field = { 40 | path: path.replace(/.properties./g, '.').replace(/.items/g, '[*]'), 41 | type: jpValue[config.typeFieldName], 42 | format: jpValue[config.formatFieldName], 43 | default: jpValue[config.defaultFieldName], 44 | }; 45 | 46 | const isExclude = config.excludePaths.some(excludePath => Object.entries(excludePath).every(([key, value]) => field[key] === value),); 47 | if (!isExclude) { 48 | fields.push(field); 49 | } 50 | }); 51 | logger.debug(`Fields that must be normalized ${JSON.stringify(fields)}`); 52 | return fields; 53 | }; 54 | 55 | /** 56 | * Method that replace all '$ref' references by definition to obtain a full schema without '$ref'. 57 | * @param schema 58 | * @returns {object} 59 | */ 60 | const deReferenceSchema = async schema => new Promise(resolve => { 61 | deRef(schema, (err, fullSchema) => { 62 | resolve(fullSchema); 63 | }); 64 | }); 65 | 66 | module.exports = { 67 | getFieldsToNormalize, 68 | deReferenceSchema, 69 | _getFieldPaths, // For unit test only 70 | }; 71 | -------------------------------------------------------------------------------- /tests/unit/lib/core/converter-to-boolean.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | 3 | describe('normalizer.js', () => { 4 | it('try to normalize json data with some boolean values', async() => { 5 | // Given 6 | const jsonToNormalize = { 7 | fields: { 8 | boolTrueFromString: 'true', 9 | boolFalseFromString: 'false', 10 | boolTrueFromUpperCaseString: 'TRUE', 11 | boolFalseFromUpperCaseString: 'FALSE', 12 | boolTrueFromInteger: 1, 13 | boolFalseFromInteger: 0, 14 | boolTrue: true, 15 | boolFalse: false 16 | } 17 | }; 18 | const jsonSchema = { 19 | fields: { 20 | type: 'object', 21 | properties: { 22 | boolTrueFromString: { 23 | type: 'boolean' 24 | }, 25 | boolFalseFromString: { 26 | type: 'boolean' 27 | }, 28 | boolTrueFromUpperCaseString: { 29 | type: 'boolean' 30 | }, 31 | boolFalseFromUpperCaseString: { 32 | type: 'boolean' 33 | }, 34 | boolTrueFromInteger: { 35 | type: 'boolean' 36 | }, 37 | boolFalseFromInteger: { 38 | type: 'boolean' 39 | }, 40 | boolTrue: { 41 | type: 'boolean' 42 | }, 43 | boolFalse: { 44 | type: 'boolean' 45 | } 46 | } 47 | } 48 | }; 49 | // When 50 | const result = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema); 51 | // Then 52 | expect(typeof result.fields.boolTrueFromString === 'boolean') 53 | .toBe(true); 54 | expect(typeof result.fields.boolTrueFromUpperCaseString === 'boolean') 55 | .toBe(true); 56 | expect(typeof result.fields.boolFalseFromString === 'boolean') 57 | .toBe(true); 58 | expect(typeof result.fields.boolFalseFromUpperCaseString === 'boolean') 59 | .toBe(true); 60 | expect(typeof result.fields.boolTrueFromInteger === 'boolean') 61 | .toBe(true); 62 | expect(typeof result.fields.boolFalseFromInteger === 'boolean') 63 | .toBe(true); 64 | expect(typeof result.fields.boolTrue === 'boolean') 65 | .toBe(true); 66 | expect(typeof result.fields.boolFalse === 'boolean') 67 | .toBe(true); 68 | expect(result.fields.boolTrueFromString).toEqual(true); 69 | expect(result.fields.boolFalseFromString).toEqual(false); 70 | expect(result.fields.boolTrueFromUpperCaseString).toEqual(true); 71 | expect(result.fields.boolFalseFromUpperCaseString).toEqual(false); 72 | expect(result.fields.boolTrueFromInteger).toEqual(true); 73 | expect(result.fields.boolFalseFromInteger).toEqual(false); 74 | expect(result.fields.boolTrue).toEqual(true); 75 | expect(result.fields.boolFalse).toEqual(false); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/unit/lib/core/types/number.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../../index'); 2 | const { NodeTypes } = require('../../../../../lib/core/types'); 3 | 4 | describe('types.js', () => { 5 | it(`must convert 'String' to 'Number'`, () => { 6 | // Given 7 | let jsonToNormalize = { fieldToNormalize: '123' }; 8 | const targetType = NodeTypes.NUMBER_TYPE; 9 | // When 10 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 11 | // Then 12 | expect(jsonToNormalize).toEqual({ fieldToNormalize: 123 }); 13 | }); 14 | 15 | it(`must convert 'String' to 'Number' (Decimal Support)`, () => { 16 | // Given 17 | let jsonToNormalize = { fieldToNormalize: '123.23' }; 18 | const targetType = NodeTypes.NUMBER_TYPE; 19 | // When 20 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 21 | // Then 22 | expect(jsonToNormalize).toEqual({ fieldToNormalize: 123.23 }); 23 | }); 24 | 25 | it(`must convert 'String' to 'Number' (Decimal Support)`, () => { 26 | // Given 27 | let jsonToNormalize = { fieldToNormalize: '123,23' }; 28 | const targetType = NodeTypes.NUMBER_TYPE; 29 | // When 30 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 31 | // Then 32 | expect(jsonToNormalize).toEqual({ fieldToNormalize: 123.23 }); 33 | }); 34 | 35 | it(`must keep correct number value`, () => { 36 | // Given 37 | let jsonToNormalize = { fieldToNormalize: 123 }; 38 | const targetType = NodeTypes.NUMBER_TYPE; 39 | // When 40 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 41 | // Then 42 | expect(jsonToNormalize).toEqual({ fieldToNormalize: 123 }); 43 | }); 44 | 45 | it(`must keep correct number value (Decimal support)`, () => { 46 | // Given 47 | let jsonToNormalize = { fieldToNormalize: 123.123 }; 48 | const targetType = NodeTypes.NUMBER_TYPE; 49 | // When 50 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 51 | // Then 52 | expect(jsonToNormalize).toEqual({ fieldToNormalize: 123.123 }); 53 | }); 54 | 55 | it(`must convert object to NaN`, () => { 56 | // Given 57 | let jsonToNormalize = { fieldToNormalize: { impossibleToConvertToNumber: true } }; 58 | const targetType = NodeTypes.NUMBER_TYPE; 59 | // When 60 | jsonToNormalize = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToNormalize, paths: ['fieldToNormalize'], type: targetType }); 61 | // Then 62 | expect(jsonToNormalize).toEqual({ fieldToNormalize: NaN }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | **Examples of unacceptable behavior include:** 29 | 30 | * _The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind_ 32 | * _Trolling, insulting or derogatory comments, and personal or political attacks 33 | Public or private harassment_ 34 | * _Publishing others' private information, such as a physical or email address, 35 | without their explicit permission_ 36 | * _Other conduct which could reasonably be considered inappropriate in a 37 | professional setting_ 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Main contributors of this repository are responsible for clarifying and enforcing these standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Main contributors of this repository have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Attribution 52 | 53 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 54 | version 2.1, available at 55 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 56 | 57 | For answers to common questions about this code of conduct, see the FAQ at 58 | [https://www.contributor-covenant.org/faq][FAQ]. 59 | 60 | Translations are available at 61 | [https://www.contributor-covenant.org/translations][translations]. 62 | 63 | [homepage]: https://www.contributor-covenant.org 64 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 65 | [FAQ]: https://www.contributor-covenant.org/faq 66 | [translations]: https://www.contributor-covenant.org/translations 67 | -------------------------------------------------------------------------------- /tests/unit/lib/core/converter-to-string.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | 3 | describe('normalizer.js', () => { 4 | it('try to normalize json data to string', async() => { 5 | // Given 6 | const jsonToNormalize = { 7 | fields: { 8 | stringValue: 'OK', 9 | numberValue: 2, 10 | arrayValue: ['12'], 11 | booleanValue: true, 12 | objectWithToString: { 13 | toString: () => 'OK' 14 | }, 15 | objectWithoutToString: { 16 | firstName: 'TEST', 17 | lastName: 'TEST', 18 | }, 19 | emptyObject: {}, 20 | nullObject: null, 21 | undefinedObject: undefined 22 | } 23 | }; 24 | const jsonSchema = { 25 | fields: { 26 | type: 'object', 27 | properties: { 28 | stringValue: { 29 | type: 'string' 30 | }, 31 | numberValue: { 32 | type: 'string' 33 | }, 34 | arrayValue: { 35 | type: 'string' 36 | }, 37 | booleanValue: { 38 | type: 'string' 39 | }, 40 | objectWithToString: { 41 | type: 'string' 42 | }, 43 | objectWithoutToString: { 44 | type: 'string' 45 | }, 46 | emptyObject: { 47 | type: 'string' 48 | }, 49 | nullObject: { 50 | type: 'string' 51 | }, 52 | undefinedObject: { 53 | type: 'string' 54 | } 55 | } 56 | } 57 | }; 58 | // When 59 | const result = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema); 60 | // Then 61 | expect(typeof result.fields.stringValue === 'string') 62 | .toBe(true); 63 | expect(typeof result.fields.numberValue === 'string') 64 | .toBe(true); 65 | expect(typeof result.fields.arrayValue === 'string') 66 | .toBe(true); 67 | expect(typeof result.fields.booleanValue === 'string') 68 | .toBe(true); 69 | expect(typeof result.fields.objectWithToString === 'string') 70 | .toBe(true); 71 | expect(typeof result.fields.objectWithoutToString === 'string') 72 | .toBe(true); 73 | expect(typeof result.fields.emptyObject === 'string') 74 | .toBe(true); 75 | expect(typeof result.fields.nullObject === 'object') 76 | .toBe(true); 77 | expect(typeof result.fields.undefinedObject === 'undefined') 78 | .toBe(true); 79 | 80 | expect(result.fields.stringValue).toEqual('OK'); 81 | expect(result.fields.numberValue).toEqual('2'); 82 | expect(result.fields.arrayValue).toEqual(`["12"]`); 83 | expect(result.fields.booleanValue).toEqual('true'); 84 | expect(result.fields.objectWithToString).toEqual('OK'); 85 | expect(result.fields.objectWithoutToString).toEqual(`{"firstName":"TEST","lastName":"TEST"}`); 86 | expect(result.fields.emptyObject).toEqual(''); 87 | expect(result.fields.nullObject).toEqual(null); 88 | expect(result.fields.undefinedObject).toEqual(undefined); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /lib/core/types.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('../../config/logger'); 2 | 3 | const NodeTypes = { 4 | ARRAY_TYPE: 'array', 5 | NUMBER_TYPE: 'number', 6 | INTEGER_TYPE: 'integer', 7 | BOOLEAN_TYPE: 'boolean', 8 | STRING_TYPE: 'string', 9 | NULL_TYPE: 'null' 10 | }; 11 | 12 | /** 13 | * Method that convert json object type by another one. 14 | * Ex: - Replace single json object to json object array (ARRAY_TYPE) 15 | * See 'ConversionTypes'. 16 | * @param jsonNode - json node to convert 17 | * @param type - target type 18 | */ 19 | const convertType = (jsonNode, type) => { 20 | logger.debug(`Normalize ${JSON.stringify(jsonNode)} to '${type}'`); 21 | switch (type) { 22 | // Convert single json object to json object array 23 | case NodeTypes.ARRAY_TYPE: 24 | return !Array.isArray(jsonNode) ? [jsonNode] : jsonNode; 25 | case NodeTypes.NUMBER_TYPE: 26 | return _convertToNumber(jsonNode); 27 | case NodeTypes.INTEGER_TYPE: 28 | return !Number.isInteger(jsonNode) ? parseInt(jsonNode, 10) : jsonNode; 29 | case NodeTypes.BOOLEAN_TYPE: 30 | return _convertToBoolean(jsonNode); 31 | case NodeTypes.STRING_TYPE: 32 | return _convertToString(jsonNode); 33 | case NodeTypes.NULL_TYPE: 34 | return null; 35 | default: 36 | } 37 | return jsonNode; 38 | }; 39 | 40 | /** 41 | * Method that convert jsonNode to Boolean type 42 | * @param jsonNode 43 | * @returns {boolean} 44 | * @private 45 | */ 46 | const _convertToBoolean = (jsonNode) => { 47 | if (typeof jsonNode === 'boolean') { return jsonNode; } 48 | const jsonNodeType = typeof jsonNode; 49 | const jsonNodeTypeIsString = jsonNodeType === 'string'; 50 | if (jsonNodeTypeIsString) { 51 | const jsonNodeValue = jsonNode.toLowerCase(); 52 | if (jsonNodeValue === 'true') { 53 | return true; 54 | } 55 | return false; 56 | } 57 | return Boolean(jsonNode); 58 | }; 59 | 60 | /** 61 | * Method that convert jsonNode to String type 62 | * In case of jsonNode is not an object : 63 | * - Returns the string value 64 | * In case of jsonNode is an object : 65 | * - Returns '' if jsonNode is an empty object 66 | * - Returns toString method result if exists 67 | * - Returns JSON.Stringify result if not 68 | * @param {object} jsonNode 69 | * @returns {string | null} 70 | * @private 71 | */ 72 | const _convertToString = (jsonNode) => { 73 | if (typeof jsonNode === 'undefined') { return undefined; } 74 | const isObject = (typeof jsonNode === 'object'); 75 | if (!isObject) { return `${jsonNode}`; } 76 | if (jsonNode === null) { return null; } 77 | 78 | if (!Object.keys(jsonNode).length) { return ''; } 79 | // eslint-disable-next-line no-prototype-builtins 80 | if (jsonNode.hasOwnProperty('toString')) { return jsonNode.toString(); } 81 | return JSON.stringify(jsonNode); 82 | }; 83 | 84 | /** 85 | * Method that convert jsonNode to Json Number type 86 | * @param jsonNode 87 | * @returns {number} 88 | * @private 89 | */ 90 | const _convertToNumber = (jsonNode) => { 91 | let result = jsonNode; 92 | const jsonNodeType = typeof jsonNode; 93 | const jsonNodeTypeIsNumber = jsonNodeType === 'number'; 94 | 95 | if (!jsonNodeTypeIsNumber) { 96 | const jsonNodeTypeIsString = jsonNodeType === 'string'; 97 | result = jsonNodeTypeIsString ? result.replace(/,/g, '.') : result; 98 | result = parseFloat(result); 99 | } 100 | 101 | return result; 102 | }; 103 | 104 | module.exports = { 105 | NodeTypes, 106 | convertType 107 | }; 108 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | export default [ 5 | { 6 | files: ["**/*.js"], 7 | languageOptions: { 8 | sourceType: "commonjs", 9 | ecmaVersion: 2022 10 | }, 11 | plugins: {}, 12 | rules: { 13 | "quotes": 'off', 14 | "no-underscore-dangle": 'off', 15 | "comma-dangle": 'off', 16 | "no-use-before-define": 'off', 17 | "class-methods-use-this": 'off', 18 | "linebreak-style": ["error", "unix"], 19 | "arrow-parens": 'off', 20 | "max-len": ["error", { "code": 140, "tabWidth": 4, "comments": 100, "ignoreUrls": true }], 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "semi": ["error", "always"], 23 | "no-console": 0, 24 | "block-scoped-var": "error", 25 | "curly": ["error", "multi-line"], 26 | "eqeqeq": "error", 27 | "guard-for-in": "error", 28 | "no-alert": "error", 29 | "no-multi-spaces": "error", 30 | "no-return-await": "error", 31 | "no-return-assign": "error", 32 | "no-script-url": "error", 33 | "no-self-assign": "error", 34 | "no-self-compare": "error", 35 | "wrap-iife": "error", 36 | "yoda": "error", 37 | "no-shadow": 1, 38 | "no-label-var": "error", 39 | "array-bracket-newline": ["error", {"multiline": true}], 40 | "array-bracket-spacing": ["error", "never"], 41 | "array-element-newline": ["error", {"multiline": true}], 42 | "block-spacing": ["error", "always"], 43 | "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], 44 | "comma-spacing": ["error", {"before": false, "after": true}], 45 | "comma-style": ["error", "last"], 46 | "eol-last": ["error", "always"], 47 | "func-call-spacing": ["error", "never"], 48 | "implicit-arrow-linebreak": ["error", "beside"], 49 | "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], 50 | "keyword-spacing": ["error", {"before": true, "after": true}], 51 | "multiline-comment-style": ["error", "starred-block"], 52 | "no-array-constructor": "error", 53 | "no-lonely-if": "error", 54 | "no-multi-assign": "error", 55 | "no-multiple-empty-lines": ["error", {"max": 2}], 56 | "no-new-object": "error", 57 | "no-trailing-spaces": "error", 58 | "no-unneeded-ternary": "error", 59 | "no-whitespace-before-property": "error", 60 | "object-curly-spacing": ["error", "always", {"arraysInObjects": false, "objectsInObjects": true}], 61 | "operator-assignment": ["error", "always"], 62 | "padded-blocks": ["error", "never"], 63 | "spaced-comment": ["error", "always"], 64 | "arrow-body-style": ["error", "as-needed"], 65 | "arrow-spacing": ["error", {"before": true, "after": true}], 66 | "no-confusing-arrow": ["error", {"allowParens": true}], 67 | "no-duplicate-imports": "error", 68 | "no-var": "error", 69 | "object-shorthand": "error", 70 | "prefer-arrow-callback": "error", 71 | "prefer-const": "error", 72 | "prefer-destructuring": ["error", {"object": true, "array": true}], 73 | "prefer-rest-params": "error", 74 | "prefer-spread": "error", 75 | "prefer-template": "error", 76 | "quote-props": ["error", "as-needed"], 77 | "semi-spacing": "error", 78 | "semi-style": ["error", "last"], 79 | "space-before-blocks": ["error", "always"], 80 | "space-before-function-paren": ["error", "never"], 81 | "space-in-parens": ["error", "never"] 82 | } 83 | }, 84 | { 85 | languageOptions: { 86 | globals: { 87 | ...globals.jest, 88 | ...globals.node 89 | } 90 | } 91 | }, 92 | pluginJs.configs.recommended 93 | ]; 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.0.13] 8 | - Add a new 'exclude' option to ignore some fields/paths. [#62](https://github.com/benjamin-allion/json-node-normalizer/pull/62) 9 | - Default value is not set anymore for boolean field with 'false' value. [#57](https://github.com/benjamin-allion/json-node-normalizer/pull/57) 10 | 11 | ## [1.0.12] 12 | - Fix cache problem (Add cloneDeep to avoid deleted ref problem). [#61](https://github.com/benjamin-allion/json-node-normalizer/pull/61) 13 | 14 | ## [1.0.11] 15 | - Replace 'cacheId' path param by a value [#60](https://github.com/benjamin-allion/json-node-normalizer/pull/60) 16 | 17 | ## [1.0.10] 18 | - New cache option to increase performance. [#58](https://github.com/benjamin-allion/json-node-normalizer/issues/58) 19 | - New 'clearCache' method 20 | 21 | See README.MD documentation for more information about cache function. 22 | 23 | ## [1.0.9] 24 | - Dependencies update 25 | 26 | ## [1.0.8] - 2021-11-16 27 | - Fix default value set for boolean field with false. [#52](https://github.com/benjamin-allion/json-node-normalizer/issues/52) 28 | - Fix dependencies security vulnerability. 29 | 30 | ## [1.0.7] - 2021-04-12 31 | - Add 'default' value support. [#38](https://github.com/benjamin-allion/json-node-normalizer/issues/38) 32 | - Full 'normalizer.js' refactoring [#41](https://github.com/benjamin-allion/json-node-normalizer/issues/41) 33 | - Replace 'normalizePath' function by 'normalizePaths' [#41](https://github.com/benjamin-allion/json-node-normalizer/issues/41) 34 | - New signature for 'normalizeNode' function [#41](https://github.com/benjamin-allion/json-node-normalizer/issues/41) 35 | - Update documentation for 'normalizePaths' & default values [#41](https://github.com/benjamin-allion/json-node-normalizer/issues/41) 36 | 37 | ## [1.0.6] - 2021-03-31 38 | - Fix dependencies security vulnerability. 39 | - Upgrade all dependencies to last version 40 | - Add new string converter to support object -> string conversion and avoid '[object Object]' values (see #34) 41 | 42 | ## [1.0.5] - 2019-12-29 43 | - Fix dependencies security vulnerability. (See #28) 44 | 45 | ## [1.0.4] - 2019-11-06 46 | 47 | ### Major bugfix 48 | - Fix boolean not correctly normalized / converted. 49 | See [#26](https://github.com/benjamin-allion/json-node-normalizer/issues/26) 50 | - Fix number with decimal normalization support. See [#24](https://github.com/benjamin-allion/json-node-normalizer/issues/24) 51 | - Fix JsonNodeNormalizer configuration support. 52 | 53 | ### Added 54 | - String formatting support (See #14, #15). 55 | You can now normalize string type into lowercase / uppercase. 56 | - Refactoring & fix some methods documentation problems. 57 | - 'oasFlatten' method to flat the definition (for Swagger 2 & Openapi 3 specifications support) 58 | 59 | ## [1.0.3] - 2019-07-23 60 | ### Added 61 | - Normalization field type configuration support (See #13). 62 | - Optimising dependencies : 63 | Remove useless development dependencies. 64 | Upgrade all dependencies to latest versions. 65 | Fix 'lodash' development dependency security vulnerability. (See #16) 66 | 67 | ## [1.0.2] - 2019-06-23 68 | ### Added 69 | - 'null' type support 70 | - Optimising dependencies : 71 | Remove 'codacy-coverage' dep. from release version. 72 | 73 | ## [1.0.1] - 2019-06-17 74 | ### Added 75 | - Logging level support 76 | - Documentation about logging level 77 | 78 | ## [1.0.0] - 2019-06-16 79 | ### Added 80 | - First release 81 | - Normalization by Json Schema 82 | - Normalization by Json Path 83 | - Json-Schema $Ref Support 84 | - Type support : number, integer, string, array, boolean 85 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_allOf_anyOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/components/schemas/Pet" 19 | }, 20 | "discriminator": { 21 | "propertyName": "pet_type" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/pets/:id": { 31 | "get": { 32 | "responses": { 33 | "200": { 34 | "description": "1 pet", 35 | "content": { 36 | "application/json": { 37 | "schema": { 38 | "$ref": "#/components/schemas/Pet" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "components": { 48 | "schemas": { 49 | "Animal": { 50 | "type": "object", 51 | "required": [ 52 | "race", 53 | "id" 54 | ], 55 | "properties": { 56 | "race": { 57 | "type": "string" 58 | }, 59 | "id": { 60 | "type": "integer" 61 | }, 62 | "measurement": { 63 | "$ref": "#/components/schemas/measurement" 64 | } 65 | } 66 | }, 67 | "Pet": { 68 | "anyOf": [ 69 | { 70 | "$ref": "#/components/schemas/Dog" 71 | }, 72 | { 73 | "type": "object", 74 | "properties": { 75 | "id": { 76 | "type": "integer" 77 | }, 78 | "name": { 79 | "type": "string" 80 | }, 81 | "isKnown": { 82 | "type": "boolean" 83 | } 84 | }, 85 | "required": [ 86 | "id" 87 | ] 88 | }, 89 | { 90 | "type": "object", 91 | "properties": { 92 | "id": { 93 | "type": "integer" 94 | }, 95 | "isKnown": { 96 | "type": "boolean" 97 | } 98 | }, 99 | "required": [ 100 | "id" 101 | ] 102 | } 103 | ] 104 | }, 105 | "Dog": { 106 | "allOf": [ 107 | { 108 | "$ref": "#/components/schemas/Animal" 109 | }, 110 | { 111 | "type": "object", 112 | "properties": { 113 | "isKnown": { 114 | "type": "boolean" 115 | } 116 | }, 117 | "required": [ 118 | "isKnown" 119 | ] 120 | }, 121 | { 122 | "type": "object", 123 | "properties": { 124 | "category": { 125 | "type": "string" 126 | }, 127 | "name": { 128 | "type": "string" 129 | } 130 | }, 131 | "required": [ 132 | "name" 133 | ] 134 | } 135 | ] 136 | }, 137 | "measurement": { 138 | "type": "object", 139 | "properties": { 140 | "height": { 141 | "type": "integer" 142 | }, 143 | "weight": { 144 | "type": "integer" 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_allof_oneof.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "#/components/schemas/Pet" 19 | }, 20 | "discriminator": { 21 | "propertyName": "pet_type" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/pets/:id": { 31 | "get": { 32 | "responses": { 33 | "200": { 34 | "description": "1 pet", 35 | "content": { 36 | "application/json": { 37 | "schema": { 38 | "$ref": "#/components/schemas/Pet" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "components": { 48 | "schemas": { 49 | "Animal": { 50 | "type": "object", 51 | "required": [ 52 | "race", 53 | "id" 54 | ], 55 | "properties": { 56 | "race": { 57 | "type": "string" 58 | }, 59 | "id": { 60 | "type": "integer" 61 | }, 62 | "measurement": { 63 | "$ref": "#/components/schemas/measurement" 64 | } 65 | } 66 | }, 67 | "Pet": { 68 | "oneOf": [ 69 | { 70 | "$ref": "#/components/schemas/Dog" 71 | }, 72 | { 73 | "type": "object", 74 | "properties": { 75 | "id": { 76 | "type": "integer" 77 | }, 78 | "name": { 79 | "type": "string" 80 | }, 81 | "isKnown": { 82 | "type": "boolean" 83 | } 84 | }, 85 | "required": [ 86 | "id" 87 | ] 88 | }, 89 | { 90 | "type": "object", 91 | "properties": { 92 | "id": { 93 | "type": "integer" 94 | }, 95 | "isKnown": { 96 | "type": "boolean" 97 | } 98 | }, 99 | "required": [ 100 | "id" 101 | ] 102 | } 103 | ] 104 | }, 105 | "Dog": { 106 | "allOf": [ 107 | { 108 | "$ref": "#/components/schemas/Animal" 109 | }, 110 | { 111 | "type": "object", 112 | "properties": { 113 | "isKnown": { 114 | "type": "boolean" 115 | } 116 | }, 117 | "required": [ 118 | "isKnown" 119 | ] 120 | }, 121 | { 122 | "type": "object", 123 | "properties": { 124 | "category": { 125 | "type": "string" 126 | }, 127 | "name": { 128 | "type": "string" 129 | } 130 | }, 131 | "required": [ 132 | "name" 133 | ] 134 | } 135 | ] 136 | }, 137 | "measurement": { 138 | "type": "object", 139 | "properties": { 140 | "height": { 141 | "type": "integer" 142 | }, 143 | "weight": { 144 | "type": "integer" 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /tests/unit/lib/core/schema.spec.js: -------------------------------------------------------------------------------- 1 | const { deReferenceSchema, _getFieldPaths, getFieldsToNormalize } = require('../../../../lib/core/schema'); 2 | const { schemaWithRef } = require('../../mock-sample/json-schema'); 3 | 4 | describe('schema.js', () => { 5 | it('try to dereference a Json Schema', async() => { 6 | const schemaWithoutRef = await deReferenceSchema(schemaWithRef); 7 | expect(schemaWithRef.properties.vegetables.items).not.toHaveProperty('properties'); 8 | expect(schemaWithRef.properties.vegetables.items).toHaveProperty('$ref'); 9 | expect(schemaWithoutRef.properties.vegetables.items.properties).toHaveProperty('veggieName'); 10 | expect(schemaWithoutRef.properties.vegetables.items.properties).toHaveProperty('veggieLike'); 11 | expect(schemaWithoutRef.properties.vegetables.items.properties).toHaveProperty('veggieColor'); 12 | }); 13 | }); 14 | 15 | describe('schema.js', () => { 16 | it('try to get all field types from Json Schema', async() => { 17 | const schemaWithoutRef = await deReferenceSchema(schemaWithRef); 18 | const typePaths = _getFieldPaths(schemaWithoutRef); 19 | expect(typePaths).toContainEqual('$.properties.fruits'); 20 | expect(typePaths).toContainEqual('$.properties.vegetables'); 21 | expect(typePaths).toContainEqual('$.properties.dateOfPurchase'); 22 | expect(typePaths).toContainEqual('$.properties.fruits.items'); 23 | expect(typePaths).toContainEqual('$.properties.vegetables.items.properties.veggieName'); 24 | expect(typePaths).toContainEqual('$.properties.vegetables.items.properties.veggieLike'); 25 | expect(typePaths).toContainEqual('$.properties.vegetables.items.properties.veggieColor'); 26 | expect(typePaths).not.toContainEqual('$.properties.other'); 27 | expect(typePaths).not.toContainEqual('$'); 28 | expect(typePaths).not.toContainEqual('$.definition.veggie'); 29 | expect(typePaths).not.toContainEqual('$.definition.veggie.items.properties.veggieName'); 30 | }); 31 | }); 32 | 33 | describe('schema.js', () => { 34 | it('try to get all fields that must be normalized from Json-Schema, using default config', async() => { 35 | const schemaWithoutRef = await deReferenceSchema(schemaWithRef); 36 | const fields = getFieldsToNormalize(schemaWithoutRef); 37 | 38 | expect(fields).toContainEqual({ path: '$.fruits', type: 'array' }); 39 | expect(fields).toContainEqual({ path: '$.vegetables', type: 'array' }); 40 | expect(fields).toContainEqual({ path: '$.vegetables[*].veggieName', type: 'string' }); 41 | expect(fields).toContainEqual({ path: '$.vegetables[*].veggieLike', type: 'boolean' }); 42 | expect(fields).toContainEqual({ path: '$.vegetables[*].veggieColor', type: 'string' }); 43 | expect(fields).toContainEqual({ 44 | path: '$.dateOfPurchase', 45 | type: 'string', 46 | format: 'date-time', 47 | }); 48 | expect(fields).not.toContainEqual({ path: '$.other', type: 'object' }); 49 | expect(fields).not.toContainEqual({ path: '$', type: 'object' }); 50 | expect(fields).not.toContainEqual({ path: '$.definition.veggie', type: 'object' }); 51 | expect(fields).not.toContainEqual({ path: '$.definition.veggie.veggieName', type: 'string' }); 52 | }); 53 | 54 | it('try to get all fields that must be normalized from Json-Schema, using excludePaths', async() => { 55 | const schemaWithoutRef = await deReferenceSchema(schemaWithRef); 56 | const fields = getFieldsToNormalize(schemaWithoutRef, { 57 | excludePaths: [{ type: 'string', format: 'date-time' }, { path: '$.vegetables[*].veggieColor' }], 58 | }); 59 | 60 | expect(fields).toContainEqual({ path: '$.fruits', type: 'array' }); 61 | expect(fields).toContainEqual({ path: '$.vegetables', type: 'array' }); 62 | expect(fields).toContainEqual({ path: '$.vegetables[*].veggieName', type: 'string' }); 63 | expect(fields).toContainEqual({ path: '$.vegetables[*].veggieLike', type: 'boolean' }); 64 | expect(fields).not.toContainEqual({ path: '$.vegetables[*].veggieColor', type: 'string' }); 65 | expect(fields).not.toContainEqual({ 66 | path: '$.dateOfPurchase', 67 | type: 'string', 68 | format: 'date-time', 69 | }); 70 | expect(fields).not.toContainEqual({ path: '$.other', type: 'object' }); 71 | expect(fields).not.toContainEqual({ path: '$', type: 'object' }); 72 | expect(fields).not.toContainEqual({ path: '$.definition.veggie', type: 'object' }); 73 | expect(fields).not.toContainEqual({ path: '$.definition.veggie.veggieName', type: 'string' }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_basic_flattered.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "required": [ 20 | "race", 21 | "id" 22 | ], 23 | "properties": { 24 | "race": { 25 | "type": "string" 26 | }, 27 | "id": { 28 | "type": "integer" 29 | }, 30 | "name": { 31 | "type": "string" 32 | }, 33 | "isKnown": { 34 | "type": "boolean" 35 | }, 36 | "measurement": { 37 | "type": "object", 38 | "properties": { 39 | "height": { 40 | "type": "integer" 41 | }, 42 | "weight": { 43 | "type": "integer" 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | "discriminator": { 50 | "propertyName": "pet_type" 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "/pets/:id": { 60 | "get": { 61 | "responses": { 62 | "200": { 63 | "description": "1 pet", 64 | "content": { 65 | "application/json": { 66 | "schema": { 67 | "type": "object", 68 | "required": [ 69 | "race", 70 | "id" 71 | ], 72 | "properties": { 73 | "race": { 74 | "type": "string" 75 | }, 76 | "id": { 77 | "type": "integer" 78 | }, 79 | "name": { 80 | "type": "string" 81 | }, 82 | "isKnown": { 83 | "type": "boolean" 84 | }, 85 | "measurement": { 86 | "type": "object", 87 | "properties": { 88 | "height": { 89 | "type": "integer" 90 | }, 91 | "weight": { 92 | "type": "integer" 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "components": { 106 | "schemas": { 107 | "Animal": { 108 | "type": "object", 109 | "required": [ 110 | "race", 111 | "id" 112 | ], 113 | "properties": { 114 | "race": { 115 | "type": "string" 116 | }, 117 | "id": { 118 | "type": "integer" 119 | }, 120 | "name": { 121 | "type": "string" 122 | }, 123 | "isKnown": { 124 | "type": "boolean" 125 | }, 126 | "measurement": { 127 | "type": "object", 128 | "properties": { 129 | "height": { 130 | "type": "integer" 131 | }, 132 | "weight": { 133 | "type": "integer" 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "measurement": { 140 | "type": "object", 141 | "properties": { 142 | "height": { 143 | "type": "integer" 144 | }, 145 | "weight": { 146 | "type": "integer" 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /lib/core/oasFlatter.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const { logger } = require('../../config/logger'); 4 | 5 | /** 6 | * Method that will flatten definition/schema of an open api specification V2 or V3. 7 | * It replaces "$ref" by the full object 8 | * @param specOas - open api specification json object to flatten 9 | * @returns {object} 10 | */ 11 | const oasFlatten = (specOas) => { 12 | // We want preserve the original specOas 13 | const newSpec = _.cloneDeep(specOas); 14 | 15 | let definitions; 16 | 17 | // openApi V2 18 | if (newSpec && newSpec.definitions) { 19 | ({ definitions } = newSpec); 20 | } 21 | 22 | // openApi V3.x 23 | if (newSpec && newSpec.components && newSpec.components.schemas) { 24 | definitions = newSpec.components.schemas; 25 | } 26 | 27 | flatten(definitions, newSpec, '/'); 28 | return newSpec; 29 | }; 30 | 31 | function flatten(definitions, obj, path) { 32 | if (obj && obj.allOf && Array.isArray(obj.allOf)) { 33 | obj.type = 'object'; 34 | obj.properties = {}; 35 | Object.keys(obj.allOf).forEach((i) => { 36 | if (obj.allOf[i].$ref) { 37 | obj.$ref = obj.$ref || []; 38 | obj.$ref.push(obj.allOf[i].$ref); 39 | } 40 | else { 41 | Object.keys(obj.allOf[i]).forEach((j) => { 42 | if (typeof obj[j] === 'object' && obj[j].constructor === Object) { 43 | obj[j] = _.merge(obj[j], obj.allOf[i][j]); 44 | } 45 | else if (Array.isArray(obj[j])) { 46 | obj[j] = _.union(obj[j], obj.allOf[i][j]); 47 | } 48 | else { 49 | obj[j] = obj.allOf[i][j]; 50 | } 51 | }); 52 | } 53 | }); 54 | delete obj.allOf; 55 | } 56 | else if (obj && obj.oneOf && Array.isArray(obj.oneOf)) { 57 | obj.type = 'object'; 58 | obj.properties = {}; 59 | Object.keys(obj.oneOf).forEach((i) => { 60 | if (obj.oneOf[i].$ref) { 61 | obj.$ref = obj.$ref || []; 62 | obj.$ref.push(obj.oneOf[i].$ref); 63 | } 64 | else { 65 | Object.keys(obj.oneOf[i]).forEach((j) => { 66 | if (typeof obj[j] === 'object' && obj[j].constructor === Object) { 67 | obj[j] = _.merge(obj[j], obj.oneOf[i][j]); 68 | } 69 | else if (Array.isArray(obj[j])) { 70 | obj[j] = _.union(obj[j], obj.oneOf[i][j]); 71 | } 72 | else { 73 | obj[j] = obj.oneOf[i][j]; 74 | } 75 | }); 76 | } 77 | }); 78 | delete obj.oneOf; 79 | } 80 | else if (obj && obj.anyOf && Array.isArray(obj.anyOf)) { 81 | obj.type = 'object'; 82 | obj.properties = {}; 83 | Object.keys(obj.anyOf).forEach((i) => { 84 | if (obj.anyOf[i].$ref) { 85 | obj.$ref = obj.$ref || []; 86 | obj.$ref.push(obj.anyOf[i].$ref); 87 | } 88 | else { 89 | Object.keys(obj.anyOf[i]).forEach((j) => { 90 | if (typeof obj[j] === 'object' && obj[j].constructor === Object) { 91 | obj[j] = _.merge(obj[j], obj.anyOf[i][j]); 92 | } 93 | else if (Array.isArray(obj[j])) { 94 | obj[j] = _.union(obj[j], obj.anyOf[i][j]); 95 | } 96 | else { 97 | obj[j] = obj.anyOf[i][j]; 98 | } 99 | }); 100 | } 101 | }); 102 | delete obj.anyOf; 103 | } 104 | 105 | if (obj && typeof obj.$ref === 'string') { 106 | obj.$ref = [obj.$ref]; 107 | } 108 | 109 | if (obj && Array.isArray(obj.$ref)) { 110 | obj.$ref.forEach((val) => { 111 | const a = val.replace('#/definitions/', '').replace('#/components/schemas/', ''); 112 | if (definitions[a]) { 113 | Object.keys(definitions[a]).forEach((j) => { 114 | if (typeof obj[j] === 'object' && obj[j].constructor === Object) { 115 | obj[j] = _.merge(obj[j], definitions[a][j]); 116 | } 117 | else if (Array.isArray(obj[j])) { 118 | obj[j] = _.union(obj[j], definitions[a][j]); 119 | } 120 | else { 121 | obj[j] = definitions[a][j]; 122 | } 123 | }); 124 | } 125 | }); 126 | delete obj.$ref; 127 | flatten(definitions, obj, path); 128 | } 129 | 130 | Object.keys(obj).forEach((i) => { 131 | if (Array.isArray(obj[i]) || (typeof obj[i] === 'object' && obj[i].constructor === Object)) { 132 | logger.debug(`${path}/${i}`); 133 | flatten(definitions, obj[i], `${path}/${i}`); 134 | } 135 | }); 136 | } 137 | 138 | module.exports = { 139 | oasFlatten 140 | }; 141 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas2_basic_flattered.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "schema": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "required": [ 18 | "race", 19 | "id" 20 | ], 21 | "properties": { 22 | "race": { 23 | "type": "string" 24 | }, 25 | "id": { 26 | "type": "integer" 27 | }, 28 | "name": { 29 | "type": "string" 30 | }, 31 | "isKnown": { 32 | "type": "boolean" 33 | }, 34 | "measurement": { 35 | "type": "object", 36 | "properties": { 37 | "height": { 38 | "type": "integer" 39 | }, 40 | "weight": { 41 | "type": "integer" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "/pets/:id": { 53 | "get": { 54 | "responses": { 55 | "200": { 56 | "description": "1 pet", 57 | "schema": { 58 | "type": "object", 59 | "required": [ 60 | "race", 61 | "id" 62 | ], 63 | "properties": { 64 | "race": { 65 | "type": "string" 66 | }, 67 | "id": { 68 | "type": "integer" 69 | }, 70 | "name": { 71 | "type": "string" 72 | }, 73 | "isKnown": { 74 | "type": "boolean" 75 | }, 76 | "measurement": { 77 | "type": "object", 78 | "properties": { 79 | "height": { 80 | "type": "integer" 81 | }, 82 | "weight": { 83 | "type": "integer" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "definitions": { 95 | "Animals": { 96 | "type": "array", 97 | "items": { 98 | "type": "object", 99 | "required": [ 100 | "race", 101 | "id" 102 | ], 103 | "properties": { 104 | "race": { 105 | "type": "string" 106 | }, 107 | "id": { 108 | "type": "integer" 109 | }, 110 | "name": { 111 | "type": "string" 112 | }, 113 | "isKnown": { 114 | "type": "boolean" 115 | }, 116 | "measurement": { 117 | "type": "object", 118 | "properties": { 119 | "height": { 120 | "type": "integer" 121 | }, 122 | "weight": { 123 | "type": "integer" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "Animal": { 131 | "type": "object", 132 | "required": [ 133 | "race", 134 | "id" 135 | ], 136 | "properties": { 137 | "race": { 138 | "type": "string" 139 | }, 140 | "id": { 141 | "type": "integer" 142 | }, 143 | "name": { 144 | "type": "string" 145 | }, 146 | "isKnown": { 147 | "type": "boolean" 148 | }, 149 | "measurement": { 150 | "type": "object", 151 | "properties": { 152 | "height": { 153 | "type": "integer" 154 | }, 155 | "weight": { 156 | "type": "integer" 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "measurement": { 163 | "type": "object", 164 | "properties": { 165 | "height": { 166 | "type": "integer" 167 | }, 168 | "weight": { 169 | "type": "integer" 170 | } 171 | } 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /tests/unit/lib/core/default-value.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | 3 | describe('normalizer.js', () => { 4 | it('Should normalize and add default values if field is undefined', async() => { 5 | // Given 6 | const jsonToNormalize = { 7 | fields: { 8 | id: 123, 9 | name: 'my_name', 10 | firstName: 'firstName', 11 | addresses: [ 12 | { 13 | enable: true, 14 | details: [ 15 | { 16 | label: 'detail_without_notes' 17 | }, 18 | { 19 | label: 'detail_with_notes', 20 | notes: [{ content: 'note_test' }] 21 | }, 22 | {} // Empty object 23 | ] 24 | }, 25 | { 26 | enable: false, 27 | details: [ 28 | { 29 | label: 'detail_with_notes', 30 | notes: [{ content: 'note_test' }] 31 | }, 32 | { 33 | label: 'detail_without_notes' 34 | } 35 | ] 36 | } 37 | ], 38 | active: false 39 | } 40 | }; 41 | const emptyJsonToNormalize = { 42 | fields: { 43 | id: 123, 44 | name: 'my_name', 45 | firstName: 'firstName' 46 | } 47 | }; 48 | const jsonSchema = { 49 | fields: { 50 | type: 'object', 51 | properties: { 52 | id: { 53 | type: 'string' 54 | }, 55 | name: { 56 | type: 'string' 57 | }, 58 | firstName: { 59 | type: 'string' 60 | }, 61 | age: { 62 | type: 'number', 63 | default: '21' 64 | }, 65 | phone: { 66 | type: 'integer', 67 | default: '0660328406' 68 | }, 69 | addresses: { 70 | type: 'array', 71 | items: { 72 | details: { 73 | type: 'array', 74 | default: [], 75 | items: { $ref: '#/definitions/addressType' } 76 | }, 77 | enable: { 78 | type: 'boolean', 79 | } 80 | } 81 | }, 82 | active: { 83 | type: 'boolean', 84 | default: true 85 | }, 86 | } 87 | }, 88 | definitions: { 89 | addressType: { 90 | label: { 91 | type: 'string', 92 | }, 93 | notes: { 94 | type: 'array', 95 | default: [], // Field that should be filled for our test case 96 | items: { 97 | content: { type: 'string' } 98 | } 99 | } 100 | } 101 | } 102 | }; 103 | 104 | // When 105 | const jsonNormalized = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema); 106 | const emptyJsonNormalized = await JsonNodeNormalizer.normalize(emptyJsonToNormalize, jsonSchema); 107 | 108 | // Then (jsonNormalized check) 109 | expect(Array.isArray(jsonNormalized.fields.addresses[0].details)).toBe(true); 110 | expect(Array.isArray(jsonNormalized.fields.addresses[0].details[0].notes)).toBe(true); 111 | expect(Array.isArray(jsonNormalized.fields.addresses[0].details[1].notes)).toBe(true); 112 | expect(Array.isArray(jsonNormalized.fields.addresses[0].details[2].notes)).toBe(true); 113 | expect(jsonNormalized.fields.addresses[0].details[0].notes).toStrictEqual([]); 114 | expect(jsonNormalized.fields.addresses[0].details[1].notes).toStrictEqual([{ content: 'note_test' }]); 115 | expect(jsonNormalized.fields.addresses[0].details[2].notes).toStrictEqual([]); 116 | expect(jsonNormalized.fields.addresses[1].details[0].notes).toStrictEqual([{ content: 'note_test' }]); 117 | expect(jsonNormalized.fields.addresses[1].details[1].notes).toStrictEqual([]); 118 | 119 | expect(Number.isInteger(jsonNormalized.fields.age)).toBe(true); 120 | expect(jsonNormalized.fields.age).toEqual(21); 121 | expect(Number.isInteger(jsonNormalized.fields.phone)).toBe(true); 122 | expect(jsonNormalized.fields.phone).toEqual(660328406); 123 | expect(typeof jsonNormalized.fields.id === 'string').toBe(true); 124 | expect(typeof jsonNormalized.fields.active === 'boolean').toBe(true); 125 | // Then (should keep field value and not set default) 126 | expect(jsonNormalized.fields.active).toBe(false); 127 | 128 | // Then (emptyJsonNormalized check) 129 | expect(emptyJsonNormalized.fields.addresses).toBeUndefined(); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/unit/lib/core/schemaCache.spec.js: -------------------------------------------------------------------------------- 1 | const SchemaCache = require('../../../../lib/core/schemaCache'); 2 | const { FormatTypes } = require('../../../../lib/core/formats'); 3 | const JsonNodeNormalizer = require('../../../../index'); 4 | 5 | /** 6 | * Async function that will be resolved after "ms" time 7 | * @param {number} ms 8 | * @returns {Promise} 9 | */ 10 | const sleepy = ms => new Promise(resolve => { setTimeout(() => resolve(), ms); }); 11 | 12 | describe('schemaCache.js', () => { 13 | it('should store object data into the cache', async() => { 14 | // Given 15 | const objectToStore = { 16 | test: "OK" 17 | }; 18 | 19 | // When 20 | SchemaCache.setData('objectTest', objectToStore, 200); 21 | SchemaCache.setData('objectTest', objectToStore, 20); // Second setData should be ignored 22 | await sleepy(100); 23 | 24 | // Then 25 | expect(SchemaCache.getData('objectTest')).toEqual(objectToStore); 26 | await sleepy(200); // Wait for cache clear 27 | }); 28 | 29 | it('should remove object from cache after delay', async() => { 30 | // Given 31 | const objectToStore = { 32 | test: "OK" 33 | }; 34 | 35 | // When 36 | SchemaCache.setData('objectTest', objectToStore, 500); 37 | await sleepy(600); 38 | 39 | // Then 40 | expect(SchemaCache.getData('objectTest')).toEqual(undefined); 41 | }); 42 | 43 | it(`'fieldsToNormalize' should be store & removed from the cache`, async() => { 44 | // Given 45 | const cacheSpy = jest.spyOn(SchemaCache, 'setData'); 46 | const jsonData = { 47 | data: { 48 | enable: 'true', 49 | lastName: 'must_be_uppercase', 50 | firstName: 'MUST_BE_LOWERCASE', 51 | } 52 | }; 53 | const jsonSchema = { 54 | data: { 55 | type: 'object', 56 | properties: { 57 | enable: { 58 | type: 'boolean' 59 | }, 60 | lastName: { 61 | type: 'string', 62 | format: FormatTypes.UPPERCASE 63 | }, 64 | firstName: { 65 | type: 'string', 66 | format: FormatTypes.LOWERCASE 67 | }, 68 | } 69 | } 70 | }; 71 | const customConfig = { 72 | useCache: true, 73 | cacheId: "mySampleSchema", 74 | cacheDuration: 800 75 | }; 76 | 77 | // When 78 | let result; 79 | for (let i = 0; i < 5; i += 1) { 80 | result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema, customConfig); 81 | } 82 | await sleepy(900); // Invalid cache 83 | 84 | for (let i = 0; i < 5; i += 1) { 85 | result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema, customConfig); 86 | } 87 | 88 | // Then 89 | const expectedResult = { 90 | data: { 91 | enable: true, 92 | lastName: 'MUST_BE_UPPERCASE', 93 | firstName: 'must_be_lowercase', 94 | } 95 | }; 96 | expect(result).toStrictEqual(expectedResult); 97 | expect(cacheSpy).toHaveBeenCalledTimes(2); 98 | await sleepy(900); // Wait for cache clear 99 | jest.clearAllMocks(); 100 | }); 101 | 102 | it(`Should clear cache between each calls`, async() => { 103 | // Given 104 | const cacheSpy = jest.spyOn(SchemaCache, 'setData'); 105 | const jsonData = { 106 | data: { 107 | enable: 'true', 108 | lastName: 'must_be_uppercase', 109 | firstName: 'MUST_BE_LOWERCASE', 110 | } 111 | }; 112 | const jsonSchema = { 113 | data: { 114 | type: 'object', 115 | properties: { 116 | enable: { 117 | type: 'boolean' 118 | }, 119 | lastName: { 120 | type: 'string', 121 | format: FormatTypes.UPPERCASE 122 | }, 123 | firstName: { 124 | type: 'string', 125 | format: FormatTypes.LOWERCASE 126 | }, 127 | } 128 | } 129 | }; 130 | const customConfig = { 131 | useCache: true, 132 | cacheId: "mySampleSchema", 133 | cacheDuration: 200 134 | }; 135 | 136 | // When 137 | let result; 138 | for (let i = 0; i < 5; i += 1) { 139 | result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema, customConfig); 140 | JsonNodeNormalizer.clearCache(); 141 | } 142 | 143 | // Then 144 | const expectedResult = { 145 | data: { 146 | enable: true, 147 | lastName: 'MUST_BE_UPPERCASE', 148 | firstName: 'must_be_lowercase', 149 | } 150 | }; 151 | expect(result).toStrictEqual(expectedResult); 152 | expect(cacheSpy).toHaveBeenCalledTimes(5); 153 | await sleepy(200); // Wait for cache clear 154 | jest.clearAllMocks(); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /tests/unit/lib/core/converter-to-array.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | const { NodeTypes } = require('../../../../index'); 3 | const jsonSample = require('../../mock-sample/json'); 4 | 5 | describe('normalizer.js', () => { 6 | it('simple field conversion \'root.subField\' (by string path)', () => { 7 | // Given 8 | const jsonToConvert = { ...jsonSample }; 9 | const targetType = NodeTypes.ARRAY_TYPE; 10 | // When 11 | const result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['root.subField'], type: targetType }); 12 | // Then 13 | expect(Array.isArray(result.root.subField)).toBe(true); 14 | expect(result.root.subField[0].mArrayProperty).toMatchObject({ value: 'TEST' }); 15 | }); 16 | }); 17 | 18 | describe('normalizer.js', () => { 19 | it('simple field conversion \'root.subField\' (by jsonPath)', () => { 20 | // Given 21 | const jsonToConvert = { ...jsonSample }; 22 | const targetType = NodeTypes.ARRAY_TYPE; 23 | // When 24 | const result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['$.root.subField'], type: targetType }); 25 | // Then 26 | expect(Array.isArray(result.root.subField)).toBe(true); 27 | expect(result.root.subField[0].mArrayProperty).toMatchObject({ value: 'TEST' }); 28 | }); 29 | }); 30 | 31 | describe('normalizer.js', () => { 32 | it('multiple fields conversion \'root.subField\'', () => { 33 | // Given 34 | const jsonToConvert = { ...jsonSample }; 35 | const targetType = NodeTypes.ARRAY_TYPE; 36 | // When 37 | const result = JsonNodeNormalizer.normalizePaths({ 38 | jsonNode: jsonToConvert, 39 | paths: ['root.subField', 'root.subArray'], 40 | type: targetType 41 | }); 42 | // Then 43 | expect(Array.isArray(result.root.subField)).toBe(true); 44 | expect(result.root.subField[0].mArrayProperty).toMatchObject({ value: 'TEST' }); 45 | }); 46 | }); 47 | 48 | describe('normalizer.js', () => { 49 | it('try to convert field that is already an array \'root.subArray\'', () => { 50 | // Given 51 | const jsonToConvert = { ...jsonSample }; 52 | const targetType = NodeTypes.ARRAY_TYPE; 53 | // When 54 | const result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['root.subArray'], type: targetType }); 55 | // Then 56 | expect(Array.isArray(result.root.subArray)).toBe(true); 57 | expect(result.root.subArray[0].mArrayProperty).toMatchObject({ value: 'TEST' }); 58 | }); 59 | }); 60 | 61 | describe('normalizer.js', () => { 62 | it('try to convert an unknown field \'root.unknown\'', () => { 63 | // Given 64 | const jsonToConvert = { ...jsonSample }; 65 | const targetType = NodeTypes.ARRAY_TYPE; 66 | // When 67 | const result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['root.unknown'], type: targetType }); 68 | // Then 69 | expect(result.root.unknown).not.toBeDefined(); 70 | }); 71 | }); 72 | 73 | describe('normalizer.js', () => { 74 | it('property of array entries conversion \'root.subArray.*.mArrayProperty\'', () => { 75 | // Given 76 | const jsonToConvert = { ...jsonSample }; 77 | const targetType = NodeTypes.ARRAY_TYPE; 78 | // When 79 | let result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['root.subArray'], type: targetType }); 80 | result = JsonNodeNormalizer.normalizePaths({ jsonNode: result, paths: ['root.subArray.*.mArrayProperty'], type: targetType }); 81 | // Then 82 | expect(Array.isArray(result.root.subArray)).toBe(true); 83 | expect(result.root.subArray[0].mProperty).toBe('TEST'); 84 | expect(Array.isArray(result.root.subArray[0].mArrayProperty)).toBe(true); 85 | expect(result.root.subArray[0].mArrayProperty[0].value).toBe('TEST'); 86 | }); 87 | }); 88 | 89 | describe('normalizer.js', () => { 90 | it('property of array of array entries conversion (multiple sub-levels) \'root.subArray.*.mArrayProperty.*.values\'', () => { 91 | // Given 92 | const jsonToConvert = { ...jsonSample }; 93 | const targetType = NodeTypes.ARRAY_TYPE; 94 | // When 95 | let result = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonToConvert, paths: ['root.subArray'], type: targetType }); 96 | result = JsonNodeNormalizer.normalizePaths({ jsonNode: result, paths: ['root.subArray.*.mArrayProperty'], type: targetType }); 97 | result = JsonNodeNormalizer.normalizePaths({ 98 | jsonNode: result, 99 | paths: ['root.subArray.*.mArrayProperty.*.values'], 100 | type: targetType 101 | }); 102 | // Then 103 | expect(Array.isArray(result.root.subArray[0].mArrayProperty)).toBe(true); 104 | expect(Array.isArray(result.root.subArray[1].mArrayProperty)).toBe(true); 105 | expect(Array.isArray(result.root.subArray[2].mArrayProperty)).toBe(true); 106 | 107 | // Doesn't contain 'values' 108 | expect(Array.isArray(result.root.subArray[0].mArrayProperty[0].values)).toBe(false); 109 | 110 | expect(Array.isArray(result.root.subArray[1].mArrayProperty[0].values)).toBe(true); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_allOf_anyOf_flattered.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "category": { 21 | "type": "string" 22 | }, 23 | "name": { 24 | "type": "string" 25 | }, 26 | "isKnown": { 27 | "type": "boolean" 28 | }, 29 | "race": { 30 | "type": "string" 31 | }, 32 | "id": { 33 | "type": "integer" 34 | }, 35 | "measurement": { 36 | "type": "object", 37 | "properties": { 38 | "height": { 39 | "type": "integer" 40 | }, 41 | "weight": { 42 | "type": "integer" 43 | } 44 | } 45 | } 46 | }, 47 | "required": [ 48 | "id", 49 | "isKnown", 50 | "name", 51 | "race" 52 | ] 53 | }, 54 | "discriminator": { 55 | "propertyName": "pet_type" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "/pets/:id": { 65 | "get": { 66 | "responses": { 67 | "200": { 68 | "description": "1 pet", 69 | "content": { 70 | "application/json": { 71 | "schema": { 72 | "type": "object", 73 | "properties": { 74 | "category": { 75 | "type": "string" 76 | }, 77 | "name": { 78 | "type": "string" 79 | }, 80 | "isKnown": { 81 | "type": "boolean" 82 | }, 83 | "race": { 84 | "type": "string" 85 | }, 86 | "id": { 87 | "type": "integer" 88 | }, 89 | "measurement": { 90 | "type": "object", 91 | "properties": { 92 | "height": { 93 | "type": "integer" 94 | }, 95 | "weight": { 96 | "type": "integer" 97 | } 98 | } 99 | } 100 | }, 101 | "required": [ 102 | "id", 103 | "isKnown", 104 | "name", 105 | "race" 106 | ] 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | }, 115 | "components": { 116 | "schemas": { 117 | "Animal": { 118 | "type": "object", 119 | "required": [ 120 | "race", 121 | "id" 122 | ], 123 | "properties": { 124 | "race": { 125 | "type": "string" 126 | }, 127 | "id": { 128 | "type": "integer" 129 | }, 130 | "measurement": { 131 | "type": "object", 132 | "properties": { 133 | "height": { 134 | "type": "integer" 135 | }, 136 | "weight": { 137 | "type": "integer" 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "Pet": { 144 | "type": "object", 145 | "properties": { 146 | "category": { 147 | "type": "string" 148 | }, 149 | "name": { 150 | "type": "string" 151 | }, 152 | "isKnown": { 153 | "type": "boolean" 154 | }, 155 | "race": { 156 | "type": "string" 157 | }, 158 | "id": { 159 | "type": "integer" 160 | }, 161 | "measurement": { 162 | "type": "object", 163 | "properties": { 164 | "height": { 165 | "type": "integer" 166 | }, 167 | "weight": { 168 | "type": "integer" 169 | } 170 | } 171 | } 172 | }, 173 | "required": [ 174 | "id", 175 | "isKnown", 176 | "name", 177 | "race" 178 | ] 179 | }, 180 | "Dog": { 181 | "type": "object", 182 | "properties": { 183 | "category": { 184 | "type": "string" 185 | }, 186 | "name": { 187 | "type": "string" 188 | }, 189 | "isKnown": { 190 | "type": "boolean" 191 | }, 192 | "race": { 193 | "type": "string" 194 | }, 195 | "id": { 196 | "type": "integer" 197 | }, 198 | "measurement": { 199 | "type": "object", 200 | "properties": { 201 | "height": { 202 | "type": "integer" 203 | }, 204 | "weight": { 205 | "type": "integer" 206 | } 207 | } 208 | } 209 | }, 210 | "required": [ 211 | "isKnown", 212 | "name", 213 | "race", 214 | "id" 215 | ] 216 | }, 217 | "measurement": { 218 | "type": "object", 219 | "properties": { 220 | "height": { 221 | "type": "integer" 222 | }, 223 | "weight": { 224 | "type": "integer" 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } -------------------------------------------------------------------------------- /tests/unit/mock-sample/oas3_allof_oneof_flattered.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "my-api", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/pets/": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "List", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "type": "array", 17 | "items": { 18 | "type": "object", 19 | "properties": { 20 | "category": { 21 | "type": "string" 22 | }, 23 | "name": { 24 | "type": "string" 25 | }, 26 | "isKnown": { 27 | "type": "boolean" 28 | }, 29 | "race": { 30 | "type": "string" 31 | }, 32 | "id": { 33 | "type": "integer" 34 | }, 35 | "measurement": { 36 | "type": "object", 37 | "properties": { 38 | "height": { 39 | "type": "integer" 40 | }, 41 | "weight": { 42 | "type": "integer" 43 | } 44 | } 45 | } 46 | }, 47 | "required": [ 48 | "id", 49 | "isKnown", 50 | "name", 51 | "race" 52 | ] 53 | }, 54 | "discriminator": { 55 | "propertyName": "pet_type" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "/pets/:id": { 65 | "get": { 66 | "responses": { 67 | "200": { 68 | "description": "1 pet", 69 | "content": { 70 | "application/json": { 71 | "schema": { 72 | "type": "object", 73 | "properties": { 74 | "category": { 75 | "type": "string" 76 | }, 77 | "name": { 78 | "type": "string" 79 | }, 80 | "isKnown": { 81 | "type": "boolean" 82 | }, 83 | "race": { 84 | "type": "string" 85 | }, 86 | "id": { 87 | "type": "integer" 88 | }, 89 | "measurement": { 90 | "type": "object", 91 | "properties": { 92 | "height": { 93 | "type": "integer" 94 | }, 95 | "weight": { 96 | "type": "integer" 97 | } 98 | } 99 | } 100 | }, 101 | "required": [ 102 | "id", 103 | "isKnown", 104 | "name", 105 | "race" 106 | ] 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | }, 115 | "components": { 116 | "schemas": { 117 | "Animal": { 118 | "type": "object", 119 | "required": [ 120 | "race", 121 | "id" 122 | ], 123 | "properties": { 124 | "race": { 125 | "type": "string" 126 | }, 127 | "id": { 128 | "type": "integer" 129 | }, 130 | "measurement": { 131 | "type": "object", 132 | "properties": { 133 | "height": { 134 | "type": "integer" 135 | }, 136 | "weight": { 137 | "type": "integer" 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "Pet": { 144 | "type": "object", 145 | "properties": { 146 | "category": { 147 | "type": "string" 148 | }, 149 | "name": { 150 | "type": "string" 151 | }, 152 | "isKnown": { 153 | "type": "boolean" 154 | }, 155 | "race": { 156 | "type": "string" 157 | }, 158 | "id": { 159 | "type": "integer" 160 | }, 161 | "measurement": { 162 | "type": "object", 163 | "properties": { 164 | "height": { 165 | "type": "integer" 166 | }, 167 | "weight": { 168 | "type": "integer" 169 | } 170 | } 171 | } 172 | }, 173 | "required": [ 174 | "id", 175 | "isKnown", 176 | "name", 177 | "race" 178 | ] 179 | }, 180 | "Dog": { 181 | "type": "object", 182 | "properties": { 183 | "category": { 184 | "type": "string" 185 | }, 186 | "name": { 187 | "type": "string" 188 | }, 189 | "isKnown": { 190 | "type": "boolean" 191 | }, 192 | "race": { 193 | "type": "string" 194 | }, 195 | "id": { 196 | "type": "integer" 197 | }, 198 | "measurement": { 199 | "type": "object", 200 | "properties": { 201 | "height": { 202 | "type": "integer" 203 | }, 204 | "weight": { 205 | "type": "integer" 206 | } 207 | } 208 | } 209 | }, 210 | "required": [ 211 | "isKnown", 212 | "name", 213 | "race", 214 | "id" 215 | ] 216 | }, 217 | "measurement": { 218 | "type": "object", 219 | "properties": { 220 | "height": { 221 | "type": "integer" 222 | }, 223 | "weight": { 224 | "type": "integer" 225 | } 226 | } 227 | } 228 | } 229 | } 230 | } -------------------------------------------------------------------------------- /lib/core/normalizer.js: -------------------------------------------------------------------------------- 1 | const jp = require('jsonpath'); 2 | const _ = require('lodash'); 3 | const SchemaCache = require('./schemaCache'); 4 | const { logger } = require('../../config/logger'); 5 | const { convertType } = require('./types'); 6 | const { normalizeFormat } = require('./formats'); 7 | const { defaultConfig } = require('../../config/normalizer'); 8 | const { deReferenceSchema, getFieldsToNormalize } = require('./schema'); 9 | const { expandArraysPaths } = require('../helpers/objectPathHelper'); 10 | 11 | /** 12 | * Method that will normalize each fields based on 'type' information 13 | * that has been described in the Json-Schema. 14 | * @param {object} jsonNode - json object to normalize 15 | * @param {object} jsonSchema - json schema that describing target object format. 16 | * @param {object} customConfig - normalization configuration (see '/config/normalizer.js') 17 | * @returns {object} 18 | */ 19 | const normalize = async(jsonNode, jsonSchema, customConfig = {}) => { 20 | const config = { ...defaultConfig, ...customConfig }; 21 | logger.debug(`Trying to normalize : '${JSON.stringify(jsonNode)}'`); 22 | logger.debug(`Based on Json-Schema information : '${JSON.stringify(jsonSchema)}'`); 23 | let jsonResult = { ...jsonNode }; 24 | 25 | let fieldsToNormalize; 26 | 27 | if (config.useCache) { 28 | const jsonSchemaId = `${config.cacheId}`; 29 | fieldsToNormalize = _.cloneDeep(SchemaCache.getData(jsonSchemaId)); 30 | 31 | if (!fieldsToNormalize) { 32 | const deReferencedSchema = await deReferenceSchema(jsonSchema); 33 | fieldsToNormalize = getFieldsToNormalize(deReferencedSchema, config); 34 | SchemaCache.setData(jsonSchemaId, fieldsToNormalize, config.cacheDuration); 35 | } 36 | } 37 | 38 | if (!config.useCache) { 39 | const deReferencedSchema = await deReferenceSchema(jsonSchema); 40 | fieldsToNormalize = getFieldsToNormalize(deReferencedSchema, config); 41 | } 42 | 43 | fieldsToNormalize.forEach((field) => { 44 | jsonResult = normalizePaths({ 45 | jsonNode: jsonResult, 46 | paths: [field.path], 47 | type: field.type, 48 | format: field.format, 49 | defaultValue: field.default 50 | }); 51 | }); 52 | return jsonResult; 53 | }; 54 | 55 | /** 56 | * Converts json object fields/nodes (from node path) to specified target type. 57 | * @param {object} jsonNode - json object to convert 58 | * @param {Array} paths (Ex: ['root.field.fieldToConvert']) 59 | * @param {string} type - target field/node type, see 'types.js' 60 | * @param {string} format - target field format, see 'formats.js' 61 | * @param {object} defaultValue - target field default value 62 | * @returns {object} 63 | */ 64 | const normalizePaths = ({ 65 | jsonNode, 66 | paths, 67 | type, 68 | format = undefined, 69 | defaultValue = undefined 70 | }) => { 71 | let jsonResult = { ...jsonNode }; 72 | paths.forEach((path) => { 73 | let jsonPath = path; 74 | logger.debug(`Start transformation of field(s) '${path}' to '${type}'`); 75 | // Convert String path format to JsonPath 76 | if (!path.startsWith('$')) { 77 | jsonPath = `$.${path}`; 78 | jsonPath = jsonPath.replace(/\.\*\./g, '[*].'); 79 | } 80 | jsonResult = normalizeNode({ 81 | jsonNode: jsonResult, 82 | path: jsonPath, 83 | type, 84 | format, 85 | defaultValue 86 | }); 87 | }); 88 | return jsonResult; 89 | }; 90 | 91 | /** 92 | * Transform the type of inner json object fields (from path). 93 | * @param {object} jsonNode - json object to transform 94 | * @param {string} path - json path of object that must be replaced 95 | * @param {string} type - target field type, see 'types.js' 96 | * @param {string} format - target field format, see 'formats.js' 97 | * @param {object} defaultValue - target field default value 98 | * @returns {object} 99 | */ 100 | const normalizeNode = ({ 101 | jsonNode, 102 | path, 103 | type, 104 | format = undefined, 105 | defaultValue = undefined 106 | }) => { 107 | let jsonResult = { ...jsonNode }; 108 | const mustBeFormatted = (format !== undefined && format !== null); 109 | logger.debug(`Json object conversion method called for ${path}`); 110 | logger.debug(`Current JSON to transform : ${JSON.stringify(jsonNode)}`); 111 | 112 | if (defaultValue) { jsonResult = setDefaultValues(jsonResult, path, defaultValue); } 113 | 114 | jp.apply(jsonResult, path, (value) => { 115 | logger.debug(`Converting Node '${JSON.stringify(value)}' to '${type}'`); 116 | const normalizedNode = convertType(value, type); 117 | return mustBeFormatted ? normalizeFormat(normalizedNode, format) : normalizedNode; 118 | }); 119 | 120 | logger.debug(`Conversion complete : ${JSON.stringify(jsonResult)}`); 121 | return jsonResult; 122 | }; 123 | 124 | /** 125 | * Set all default values for a given path (that could contain array / nested objects) 126 | * @param {object} jsonNode 127 | * @param {string} path 128 | * @param {any} defaultValue 129 | */ 130 | const setDefaultValues = (jsonNode, path, defaultValue) => { 131 | let jsonResult = { ...jsonNode }; 132 | const nestedPaths = getNestedPaths(jsonResult, path); 133 | nestedPaths.forEach((nestedPath) => { 134 | jsonResult = setDefaultValueForPath(jsonResult, nestedPath, defaultValue); 135 | }); 136 | return jsonResult; 137 | }; 138 | 139 | /** 140 | * Returns nested json path from json path expression that contains array. 141 | * Example : 142 | * IN fields.addresses[*].details[*].notes 143 | * OUT [fields.addresses[0].details[0].notes, fields.addresses[0].details[1].notes...] 144 | * 145 | * @param {object} jsonNode 146 | * @param {string} path 147 | * @returns {string[]|*[]} 148 | */ 149 | const getNestedPaths = (jsonNode, path) => { 150 | if (!path.includes('[*]')) { return [path]; } 151 | const pathWithoutJpPrefix = path.replace('$.', ''); 152 | const nestedPaths = expandArraysPaths({ 153 | object: jsonNode, 154 | paths: [pathWithoutJpPrefix] 155 | }); 156 | return nestedPaths; 157 | }; 158 | 159 | /** 160 | * set default value for a given path 161 | * @param {object} jsonNode 162 | * @param {string} path 163 | * @param {any} defaultValue 164 | */ 165 | const setDefaultValueForPath = (jsonNode, path, defaultValue) => { 166 | const jsonResult = { ...jsonNode }; 167 | const fieldValue = jp.value(jsonNode, path); 168 | const isFieldDefined = fieldValue !== undefined && fieldValue !== null; 169 | 170 | if (!isFieldDefined && isParentExists(jsonNode, path)) { 171 | jp.value(jsonResult, path, defaultValue); 172 | } 173 | return jsonResult; 174 | }; 175 | 176 | /** 177 | * Returns if node parent exists for specified path 178 | * @param {object} jsonNode 179 | * @param {string} path 180 | * @returns {boolean} 181 | */ 182 | const isParentExists = (jsonNode, path) => { 183 | const parentPath = path.substring(0, path.lastIndexOf('.')); 184 | return !!jp.value(jsonNode, parentPath); 185 | }; 186 | 187 | /** 188 | * Reset the cache 189 | */ 190 | const clearCache = () => { 191 | SchemaCache.clearCache(); 192 | }; 193 | 194 | module.exports = { 195 | normalize, 196 | normalizePaths, 197 | normalizeNode, 198 | clearCache 199 | }; 200 | -------------------------------------------------------------------------------- /tests/unit/lib/core/normalizer.spec.js: -------------------------------------------------------------------------------- 1 | const JsonNodeNormalizer = require('../../../../index'); 2 | const jsonSample = require('../../mock-sample/json'); 3 | const { FormatTypes } = require('../../../../lib/core/formats'); 4 | const { NodeTypes } = require('../../../../lib/core/types'); 5 | 6 | describe('normalizer.js', () => { 7 | it('try to convert field to undefined type', () => { 8 | // Given 9 | let jsonToConvert = { ...jsonSample }; 10 | const targetType = 'UNKNOW_TYPE'; 11 | // When 12 | jsonToConvert = JsonNodeNormalizer.normalizePaths({ 13 | jsonNode: jsonToConvert, 14 | paths: ['root.subField'], 15 | type: targetType, 16 | }); 17 | // Then 18 | expect(jsonToConvert).toEqual(jsonSample); 19 | }); 20 | }); 21 | 22 | describe('normalizer.js', () => { 23 | it('try to normalize json data from json schema', async() => { 24 | // Given 25 | const jsonToNormalize = { 26 | fields: { 27 | id: 123, 28 | name: 'my_name', 29 | firstName: 'firstName', 30 | age: '31', 31 | phone: '33600000010', 32 | orders: { 33 | label: 'first_order', 34 | }, 35 | active: 'true', 36 | }, 37 | }; 38 | const jsonSchema = { 39 | fields: { 40 | type: 'object', 41 | properties: { 42 | id: { 43 | type: 'string', 44 | }, 45 | name: { 46 | type: 'string', 47 | }, 48 | firstName: { 49 | type: 'string', 50 | }, 51 | age: { 52 | type: 'number', 53 | }, 54 | phone: { 55 | type: 'integer', 56 | }, 57 | orders: { 58 | type: 'array', 59 | items: { 60 | label: { 61 | type: 'string', 62 | }, 63 | }, 64 | }, 65 | active: { 66 | type: 'boolean', 67 | }, 68 | }, 69 | }, 70 | }; 71 | // When 72 | const result = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema); 73 | // Then 74 | expect(Array.isArray(result.fields.orders)).toBe(true); 75 | expect(Number.isInteger(result.fields.age)).toBe(true); 76 | expect(Number.isInteger(result.fields.phone)).toBe(true); 77 | expect(typeof result.fields.id === 'string').toBe(true); 78 | expect(typeof result.fields.active === 'boolean').toBe(true); 79 | }); 80 | 81 | it('try to normalize json data that should not be normalized from json schema', async() => { 82 | // Given 83 | const jsonToNormalize = { 84 | fields: { 85 | id: '123', 86 | name: 'my_name', 87 | firstName: 'firstName', 88 | age: 31, 89 | phone: 33600000010, 90 | orders: { 91 | label: 'first_order', 92 | }, 93 | active: true, 94 | externalField: { 95 | label: 'missing_param', 96 | }, 97 | }, 98 | }; 99 | const jsonSchema = { 100 | fields: { 101 | type: 'object', 102 | properties: { 103 | id: { 104 | type: 'string', 105 | }, 106 | name: { 107 | type: 'string', 108 | }, 109 | firstName: { 110 | type: 'string', 111 | }, 112 | age: { 113 | type: 'number', 114 | }, 115 | phone: { 116 | type: 'integer', 117 | }, 118 | orders: { 119 | type: 'array', 120 | items: { 121 | label: { 122 | type: 'string', 123 | }, 124 | }, 125 | }, 126 | active: { 127 | type: 'boolean', 128 | }, 129 | externalField: { 130 | type: 'null', 131 | }, 132 | }, 133 | }, 134 | }; 135 | // When 136 | const result = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema); 137 | // Then 138 | expect(Array.isArray(result.fields.orders)).toBe(true); 139 | expect(Number.isInteger(result.fields.age)).toBe(true); 140 | expect(Number.isInteger(result.fields.phone)).toBe(true); 141 | expect(typeof result.fields.id === 'string').toBe(true); 142 | expect(typeof result.fields.active === 'boolean').toBe(true); 143 | expect(result.fields.externalField).toBe(null); 144 | }); 145 | 146 | it('should normalize jsonData with specific normalization type field name (See #13)', async() => { 147 | // Given 148 | const jsonData = { data: { enable: 'true' } }; 149 | const jsonSchema = { 150 | data: { 151 | type: 'object', 152 | properties: { 153 | enable: { 154 | normalization_type: 'boolean', 155 | }, 156 | }, 157 | }, 158 | }; 159 | const config = { 160 | typeFieldName: 'normalization_type', 161 | }; 162 | // When 163 | const result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema, config); 164 | // Then 165 | expect(typeof result.data.enable).toBe('boolean'); 166 | }); 167 | 168 | it('should normalize jsonData with specific normalization format field name (See #19)', async() => { 169 | // Given 170 | const jsonData = { 171 | data: { 172 | lastName: 'must_be_uppercase', 173 | firstName: 'MUST_BE_LOWERCASE', 174 | }, 175 | }; 176 | const jsonSchema = { 177 | data: { 178 | type: 'object', 179 | properties: { 180 | lastName: { 181 | type: 'string', 182 | normalization_format: FormatTypes.UPPERCASE, 183 | }, 184 | firstName: { 185 | type: 'string', 186 | normalization_format: FormatTypes.LOWERCASE, 187 | }, 188 | }, 189 | }, 190 | }; 191 | const config = { 192 | formatFieldName: 'normalization_format', 193 | }; 194 | // When 195 | const result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema, config); 196 | // Then 197 | const expectedResult = { 198 | data: { 199 | lastName: 'MUST_BE_UPPERCASE', 200 | firstName: 'must_be_lowercase', 201 | }, 202 | }; 203 | expect(result).toStrictEqual(expectedResult); 204 | }); 205 | 206 | it('should normalize jsonData from JsonSchema with type & format definitions', async() => { 207 | // Given 208 | const jsonData = { 209 | data: { 210 | enable: 'true', 211 | lastName: 'must_be_uppercase', 212 | firstName: 'MUST_BE_LOWERCASE', 213 | }, 214 | }; 215 | const jsonSchema = { 216 | data: { 217 | type: 'object', 218 | properties: { 219 | enable: { 220 | type: 'boolean', 221 | }, 222 | lastName: { 223 | type: 'string', 224 | format: FormatTypes.UPPERCASE, 225 | }, 226 | firstName: { 227 | type: 'string', 228 | format: FormatTypes.LOWERCASE, 229 | }, 230 | }, 231 | }, 232 | }; 233 | // When 234 | const result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema); 235 | // Then 236 | const expectedResult = { 237 | data: { 238 | enable: true, 239 | lastName: 'MUST_BE_UPPERCASE', 240 | firstName: 'must_be_lowercase', 241 | }, 242 | }; 243 | expect(result).toStrictEqual(expectedResult); 244 | }); 245 | 246 | it('should normalize jsonData from JsonPaths with type & format definitions', async() => { 247 | // Given 248 | const jsonData = { 249 | data: { 250 | enable: 'true', 251 | firstName: 'MUST_BE_LOWERCASE', 252 | lastName: 'must_be_uppercase', 253 | }, 254 | }; 255 | // When 256 | let result = await JsonNodeNormalizer.normalizePaths({ 257 | jsonNode: jsonData, 258 | paths: ['.enable'], 259 | type: NodeTypes.BOOLEAN_TYPE, 260 | }); 261 | result = await JsonNodeNormalizer.normalizePaths({ 262 | jsonNode: result, 263 | paths: ['.lastName'], 264 | type: NodeTypes.STRING_TYPE, 265 | format: FormatTypes.UPPERCASE, 266 | }); 267 | result = await JsonNodeNormalizer.normalizePaths({ 268 | jsonNode: result, 269 | paths: ['.firstName'], 270 | type: NodeTypes.STRING_TYPE, 271 | format: FormatTypes.LOWERCASE, 272 | }); 273 | // Then 274 | const expectedResult = { 275 | data: { 276 | enable: true, 277 | lastName: 'MUST_BE_UPPERCASE', 278 | firstName: 'must_be_lowercase', 279 | }, 280 | }; 281 | expect(result).toStrictEqual(expectedResult); 282 | }); 283 | 284 | it('should not normalize paths matching an exclude path', async() => { 285 | // Given 286 | const jsonToNormalize = { 287 | fields: { 288 | id: 123, 289 | name: 'my_name', 290 | firstName: 'firstName', 291 | age: '31', 292 | phone: '33600000010', 293 | orders: { 294 | label: 'first_order', 295 | }, 296 | active: 'true', 297 | updatedAt: 311588172, 298 | }, 299 | }; 300 | const jsonSchema = { 301 | fields: { 302 | type: 'object', 303 | properties: { 304 | id: { 305 | type: 'string', 306 | }, 307 | name: { 308 | type: 'string', 309 | }, 310 | firstName: { 311 | type: 'string', 312 | }, 313 | age: { 314 | type: 'number', 315 | }, 316 | phone: { 317 | type: 'integer', 318 | }, 319 | orders: { 320 | type: 'array', 321 | items: { 322 | label: { 323 | type: 'string', 324 | }, 325 | }, 326 | }, 327 | active: { 328 | type: 'boolean', 329 | }, 330 | updatedAt: { 331 | type: 'string', 332 | format: 'date-time', 333 | }, 334 | }, 335 | }, 336 | }; 337 | // When 338 | const result = await JsonNodeNormalizer.normalize(jsonToNormalize, jsonSchema, { 339 | excludePaths: [{ type: 'string', format: 'date-time' }, { path: '$.fields.age' }], 340 | }); 341 | 342 | // Then 343 | expect(typeof result.fields.updatedAt).toBe(typeof jsonToNormalize.fields.updatedAt); 344 | expect(result.fields.updatedAt).toBe(jsonToNormalize.fields.updatedAt); 345 | 346 | expect(typeof result.fields.age).toBe(typeof jsonToNormalize.fields.age); 347 | expect(result.fields.age).toBe(jsonToNormalize.fields.age); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Json-Node-Normalizer 2 | NodeJS module that normalize json data types from json schema specifications. 3 | 4 | [![npm version](https://img.shields.io/npm/v/json-node-normalizer.svg?style=flat-square)](https://www.npmjs.com/package/json-node-normalizer) 5 | [![CircleCI](https://circleci.com/gh/benjamin-allion/json-node-normalizer/tree/master.svg?style=shield)](https://circleci.com/gh/benjamin-allion/json-node-normalizer/tree/master) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/607fffb36855477dbbb9c8fbdc65d246)](https://app.codacy.com/app/benjamin-allion/json-node-normalizer?utm_source=github.com&utm_medium=referral&utm_content=benjamin-allion/json-node-normalizer&utm_campaign=Badge_Grade_Dashboard) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/9038356c6a6a4bae868867d1f7454ca8)](https://www.codacy.com/app/benjamin-allion/json-node-normalizer?utm_source=github.com&utm_medium=referral&utm_content=benjamin-allion/json-node-normalizer&utm_campaign=Badge_Coverage) 8 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 9 | [![Node 22.6](https://img.shields.io/badge/node%20version-22.6-green.svg)](https://nodejs.org/en/download/) 10 | [![Uses yarn](https://img.shields.io/badge/deps-yarn-blue.svg)]() 11 | [![License](https://img.shields.io/github/license/bojand/json-schema-deref.svg?style=flat-square)](https://raw.githubusercontent.com/bojand/json-schema-deref/master/LICENSE) 12 | 13 | ![Json-Node-Normalizer Schema](docs/normalizer-schema.png) 14 | 15 | ## Features 16 | 17 | * Convert / Cast Json Node type to another type : 18 | * From Json Schema Specifications 19 | * From Json Path 20 | * Supported types : 21 | * `string` 22 | * `number`, `integer` 23 | * `array` 24 | * `boolean` 25 | * `null` 26 | * Json Schema $Ref / Definitions support 27 | 28 | ## Installation 29 | 30 | Add the latest version of `json-node-normalizer` to your package.json: 31 | 32 | ```sh 33 | npm install json-node-normalizer --save 34 | ``` 35 | 36 | ## Node.js Usage 37 | 38 | ```javascript 39 | const JsonNodeNormalizer = require('json-node-normalizer'); 40 | const normalizedJson = await JsonNodeNormalizer.normalize(jsonData, jsonSchema); 41 | ``` 42 | 43 | ## Use case 44 | 45 | We have a json object with incorrect type formats : 46 | 47 | ```javascript 48 | const jsonData = { 49 | "fields":{ 50 | "id": 123, // Must be a string 51 | "name":"my_name", 52 | "firstName":"firstName", 53 | "age": "31", // Must be a number 54 | "phone": "33600000010", // Must be a number 55 | "orders": [{ 56 | // Must contain a "label" fields with default value 57 | "articles": { // Must be an array 58 | "price": "15.4" 59 | } 60 | }], 61 | "externalData": { 62 | "id": "1234" 63 | }, // Must be a null 64 | "active": "true" // Must be a boolean 65 | } 66 | } 67 | ``` 68 | 69 | We want to normalize json object to match with a Json Schema : 70 | ```javascript 71 | const jsonSchema = { 72 | "fields":{ 73 | "type":"object", 74 | "properties":{ 75 | "id":{ 76 | "type": "string" 77 | }, 78 | "name":{ 79 | "type": "string" 80 | }, 81 | "firstName":{ 82 | "type": "string" 83 | }, 84 | "age":{ 85 | "type": "number" 86 | }, 87 | "phone":{ 88 | "type": "integer" 89 | }, 90 | "orders":{ 91 | "type": "array", 92 | "items":{ 93 | "label":{ 94 | "type": "string", 95 | "default": "Empty order" 96 | }, 97 | "articles": { 98 | "type": "array", 99 | "items": { 100 | "price": { "type": "string" } 101 | } 102 | } 103 | } 104 | }, 105 | "externalData": { 106 | "type": "null" 107 | }, 108 | "active":{ 109 | "type": "boolean" 110 | } 111 | } 112 | } 113 | } 114 | ``` 115 | We can use JsonNodeNormalizer to normalize our json data : 116 | 117 | ```javascript 118 | const JsonNodeNormalizer = require('json-node-normalizer'); 119 | const result = await JsonNodeNormalizer.normalize(jsonData, jsonSchema); 120 | ``` 121 | Result : 122 | ```javascript 123 | result = { 124 | "fields":{ 125 | "id": "123", 126 | "name": "my_name", 127 | "firstName": "firstName", 128 | "age": 31, 129 | "phone": 33600000010, 130 | "orders":[{ 131 | "label": "Empty order", 132 | "articles": [{ 133 | "price": "15.4" 134 | }] 135 | }], 136 | "externalData": null, 137 | "active": true 138 | } 139 | } 140 | ``` 141 | 142 | ## Other Example 143 | 144 | Code sample : 145 | ```javascript 146 | // Given 147 | const dataToNormalize = { 148 | data: { 149 | enable: 'true' // MUST BE CONVERTED TO BOOLEAN 150 | } 151 | }; 152 | const jsonSchema = { 153 | data: { 154 | type: 'object', 155 | properties: { 156 | enable: { 157 | type: 'boolean' 158 | } 159 | } 160 | } 161 | }; 162 | const result = await JsonNodeNormalizer.normalize(dataToNormalize, jsonSchema); 163 | ``` 164 | 165 | Result : 166 | ```javascript 167 | result = { 168 | "data":{ 169 | "enable": true 170 | } 171 | } 172 | ``` 173 | 174 | You can find some other examples in 'tests' project folder. 175 | 176 | ## Normalize node(s) from path (Without Json-Schema) 177 | 178 | You can also use `normalizePaths` method if you do not want to use the schema json. 179 | 180 | ```javascript 181 | const { JsonNodeNormalizer, NodeTypes } = require('json-node-normalizer'); 182 | let normalizedJson = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonData, paths: ['.fields.id'], type: NodeTypes.NUMBER_TYPE }); 183 | normalizedJson = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonData, paths: ['.fields.orders'], type: NodeTypes.ARRAY_TYPE }); 184 | normalizedJson = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonData, paths: ['.fields.orders[*].label'], type: NodeTypes.STRING_TYPE }); 185 | 186 | // You can also normalize each element with name 'active' for example... 187 | normalizedJson = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonData, paths: ['..active'], type: NodeTypes.BOOLEAN_TYPE }); 188 | ``` 189 | 190 | ## Set default node(s) value from path (Without Json-Schema) 191 | 192 | You can also use `normalizePaths` method to set default value (if value doesn't exist). 193 | 194 | ```javascript 195 | const { JsonNodeNormalizer, NodeTypes } = require('json-node-normalizer'); 196 | let normalizedJson = JsonNodeNormalizer.normalizePaths({ jsonNode: jsonData, paths: ['.fields.orders[*].label'], type: NodeTypes.STRING_TYPE, defaultValue: 'Empty Order' }); 197 | ``` 198 | 199 | ## Play with Swagger 2 & Openapi 3 specification 200 | 201 | In Swagger 2 and Openapi 3 specification, you can use ```$ref```, ```allOf```, ```anyOf```, ```oneOf``` in definition of objects 202 | 203 | If you want use a definition of object with this key words, you need flatter the definition like this: 204 | 205 | ``` 206 | const openapi_spec_flattered = JsonNodeNormalizer.oasFlatten(openapi_spec); 207 | ``` 208 | 209 | Example with a Swagger 2 specification: 210 | ``` 211 | cont openapi_spec = require('./docs/my-swagger.json'); 212 | openapi_spec_flattered = JsonNodeNormalizer.oasFlatten(openapi_spec); 213 | ... 214 | jsonData = { 215 | id: 1 216 | name: 'Rex', 217 | color: 'brown chocolate' 218 | } 219 | ... 220 | const normalizedJson = await JsonNodeNormalizer.normalize(jsonData, openapi_spec_flattered.definitions.Pet); 221 | ... 222 | ``` 223 | 224 | ## JsonPath Documentation 225 | 226 | See https://github.com/json-path/JsonPath for more information about JsonPath expressions. 227 | 228 | ## Logging Level 229 | 230 | Logging is disabled by default (since 1.0.10). 231 | To enable logging, you must define the `JSON_NODE_NORMALIZER_DEBUG` environment to `true`. 232 | 233 | Log events can have different severity levels - in some cases, you just want to log events with at least a warning level, sometimes log lines have to be more verbose. 234 | 235 | Each level is given a specific integer priority. The higher the priority the more important the message is considered to be. 236 | 237 | | Level | Priority | 238 | |------- |---------- | 239 | | debug | 4 | 240 | | info (default) | 2 | 241 | | error | 0 | 242 | 243 | By default the logging level is set to 'info'. 244 | 245 | You can override the logging level by setting the `JSON_NODE_NORMALIZER_LOGGING_LEVEL` environment variable. 246 | 247 | ## JsonNodeNormalizer Configuration 248 | 249 | For more specific usages, you can specify some configuration parameters when you use 'normalize' method : 250 | 251 | #### Normalization type field name 252 | 253 | Could be used in case that you want to use other field than 'type' to specify the target normalization type. 254 | 255 | Code sample : 256 | ```javascript 257 | // Given 258 | const dataToNormalize = { 259 | data: { 260 | enable: 'true' // MUST BE CONVERTED TO BOOLEAN 261 | } 262 | }; 263 | const jsonSchema = { 264 | data: { 265 | type: 'object', 266 | properties: { 267 | enable: { 268 | normalization_type: 'boolean' // 'type' by default but in that case we want to use 'normalization_type' 269 | } 270 | } 271 | } 272 | }; 273 | const config = { 274 | fieldNames: { 275 | type: 'normalization_type' // Configure target normalization field name here ! 276 | } 277 | }; 278 | const result = await JsonNodeNormalizer.normalize(dataToNormalize, jsonSchema, config); 279 | ``` 280 | 281 | Result : 282 | ```javascript 283 | result = { 284 | "data":{ 285 | "enable": true 286 | } 287 | } 288 | ``` 289 | 290 | #### Exclude some fields 291 | 292 | If you need to exclude some fields to be normalized, you can use the configuration variable `excludePaths` 293 | 294 | Code sample : 295 | 296 | ```javascript 297 | // Given 298 | const dataToNormalize = { 299 | data: { 300 | enable: 'true', 301 | count: '72', 302 | other: '12', 303 | foo: 5414325, 304 | }, 305 | }; 306 | const jsonSchema = { 307 | data: { 308 | type: 'object', 309 | properties: { 310 | enable: { 311 | type: 'boolean', 312 | }, 313 | count: { 314 | type: 'number', 315 | }, 316 | other: { 317 | type: 'number', 318 | }, 319 | foo: { 320 | type: 'string', 321 | format: 'date-time', 322 | }, 323 | }, 324 | }, 325 | }; 326 | const config = { 327 | excludePaths: [ 328 | { 329 | path: '$.data.enable', // Exclude by field path 330 | }, 331 | { 332 | type: 'number', // Exclude by type 333 | }, 334 | { 335 | type: 'string', // Exclude by both type and format 336 | format: 'date-time', 337 | }, 338 | ], 339 | }; 340 | const result = await JsonNodeNormalizer.normalize(dataToNormalize, jsonSchema, config); 341 | ``` 342 | 343 | Result : 344 | 345 | ```javascript 346 | result = { 347 | data: { 348 | enable: 'true', 349 | count: '72', 350 | other: '12', 351 | foo: 5414325, 352 | }, 353 | }; 354 | ``` 355 | 356 | #### Cache to increase performance 357 | 358 | If your schema doesn't change between calls, you can enable cache to reduce process time. 359 | 360 | Configuration variables : 361 | ```javascript 362 | { 363 | useCache: true, 364 | cacheId: "schemaId", // Schema identifier used to put/get schema from cache. 365 | cacheDuration: 60000 // Cache duration in milliseconds 366 | } 367 | ``` 368 | 369 | Code sample : 370 | ```javascript 371 | // Given 372 | const dataToNormalize = { 373 | data: { 374 | enable: 'true' // MUST BE CONVERTED TO BOOLEAN 375 | } 376 | }; 377 | const jsonSchema = { 378 | schemaName: "mySchema", 379 | data: { 380 | type: 'object', 381 | properties: { 382 | enable: { 383 | normalization_type: 'boolean' // 'type' by default but in that case we want to use 'normalization_type' 384 | } 385 | } 386 | } 387 | }; 388 | const config = { 389 | fieldNames: { 390 | useCache: true, 391 | cacheId: "mySampleSchema", 392 | cacheDuration: 60000 // 60 seconds 393 | } 394 | }; 395 | const result = await JsonNodeNormalizer.normalize(dataToNormalize, jsonSchema, config); 396 | ``` 397 | 398 | Result : 399 | ```javascript 400 | result = { 401 | "data":{ 402 | "enable": true 403 | } 404 | } 405 | ``` 406 | 407 | Note : 408 | 409 | You can use ```JsonNodeNormalizer.clearCache()``` to manually reset the library cache. 410 | 411 | ## License 412 | 413 | [MIT License](http://www.opensource.org/licenses/mit-license.php). 414 | --------------------------------------------------------------------------------