├── .editorconfig ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .yarn └── releases │ └── yarn-4.9.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── help-text.json └── json-schema-to-openapi-schema.js ├── eslint.config.mjs ├── package.json ├── src ├── const.ts ├── index.ts └── types.ts ├── test ├── __snapshots__ │ ├── circular_schema.test.ts.snap │ └── dereference_schema.test.ts.snap ├── array-items.test.ts ├── circular_schema.test.ts ├── clone_schema.test.ts ├── combination_keywords.test.ts ├── complex_schemas.test.ts ├── const.test.ts ├── default-null.test.ts ├── dereference_schema.test.ts ├── examples.test.ts ├── exclusiveMinMax.test.ts ├── fixtures │ └── definitions.yaml ├── helpers.ts ├── if-then-else.test.ts ├── invalid_types.test.ts ├── items.test.ts ├── nullable.test.ts ├── pattern_properties.test.ts ├── properties.test.ts ├── readonly_writeonly.test.ts ├── rewrite_as_extensions.test.ts ├── schemas │ ├── address │ │ ├── json-schema.json │ │ └── openapi.json │ ├── basic │ │ ├── json-schema.json │ │ └── openapi.json │ ├── calendar │ │ ├── json-schema.json │ │ └── openapi.json │ ├── circular │ │ ├── json-schema.json │ │ ├── openapi-circular.json │ │ └── openapi.json │ ├── events │ │ ├── json-schema.json │ │ └── openapi.json │ ├── example2 │ │ ├── json-schema.json │ │ └── openapi.json │ └── invalid │ │ └── json-schema.json ├── subschema.test.ts ├── tsconfig.json └── type-array-split.test.ts ├── tsconfig.json ├── vite.config.mjs └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | [*.{ts,js,yaml,scenario,md}] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.{ts,js}] 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release npm package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: latest 17 | cache: 'yarn' 18 | - run: yarn install --immutable 19 | - run: yarn build 20 | - run: yarn test 21 | - run: npx semantic-release --branches main 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 20 12 | - 22 13 | - latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'yarn' 21 | - name: yarn install, build, and test 22 | run: | 23 | yarn install --immutable 24 | yarn build 25 | yarn lint 26 | yarn test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build.sh 2 | .coveralls.yml 3 | .node-version 4 | .nyc_output 5 | resolved.yaml 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage 19 | # (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directories 29 | node_modules 30 | jspm_packages 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | dist 38 | .idea 39 | .yarn/install-state.gz 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2020-01-20 9 | 10 | ### Changed 11 | 12 | - Moved over to the `@openapi-contrib` NPM organization. 13 | 14 | ## [0.4.0] - 2019-10-04 15 | 16 | ### Added 17 | 18 | - Take the first JSON Schema `example` and put in OpenAPI Schema Object `example` 19 | 20 | ## [0.3.0] - 2018-12-18 21 | 22 | ### Added 23 | 24 | - Create empty items, as it must always be present for type: array 25 | - Rewrite exclusiveMinimum/exclusiveMaximum 26 | - Rewrite if/then/else as oneOf + allOf 27 | - Rewrite const as single element enum 28 | 29 | ## [0.2.0] - 2018-05-10 30 | 31 | ### Fixed 32 | 33 | - Implemented [@cloudflare/json-schema-walker] to make sure all subschemas are 34 | processed 35 | 36 | [@cloudflare/json-schema-walker]: https://github.com/cloudflare/json-schema-tools#cloudflarejson-schema-walker 37 | 38 | ## [0.1.1] - 2018-04-09 39 | 40 | ### Added 41 | 42 | - Convert `dependencies` to an allOf + oneOf OpenAPI-valid equivalent 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Phil Sturgeon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema to OpenAPI Schema 2 | 3 | A little NodeJS package to convert JSON Schema to a [OpenAPI Schema Object](http://spec.openapis.org/oas/v3.0.3.html#schema-object). 4 | 5 | [![Treeware](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Treeware&query=%24.total&url=https%3A%2F%2Fpublic.offset.earth%2Fusers%2Ftreeware%2Ftrees)](https://treeware.earth) 6 | 7 | ## Features 8 | 9 | - converts JSON Schema Draft 04 to OpenAPI 3.0 Schema Object 10 | - switches `type: ['foo', 'null']` to `type: foo` and `nullable: true` 11 | - supports deep structures with nested `allOf`s etc. 12 | - switches `patternProperties` to `x-patternProperties` 13 | - converts `dependencies` to an allOf + oneOf OpenAPI-valid equivalent 14 | 15 | ## Installation 16 | 17 | ```shell 18 | npm install --save @openapi-contrib/json-schema-to-openapi-schema 19 | ``` 20 | 21 | Requires NodeJS v10 or greater. 22 | 23 | ## Usage 24 | 25 | Here's a small example to get the idea: 26 | 27 | ```ts 28 | import { convert } from '@openapi-contrib/json-schema-to-openapi-schema'; 29 | 30 | const schema = { 31 | $schema: 'http://json-schema.org/draft-04/schema#', 32 | type: ['string', 'null'], 33 | format: 'date-time', 34 | }; 35 | 36 | (async () => { 37 | const convertedSchema = await convert(schema); 38 | console.log(convertedSchema); 39 | })(); 40 | ``` 41 | 42 | The example prints out 43 | 44 | ```js 45 | { 46 | type: 'string', 47 | format: 'date-time', 48 | nullable: true 49 | } 50 | ``` 51 | 52 | ### Options 53 | 54 | The function accepts `options` object as the second argument. 55 | 56 | #### `cloneSchema` (boolean) 57 | 58 | If set to `false`, converts the provided schema in place. If `true`, clones the schema by converting it to JSON and back. The overhead of the cloning is usually negligible. Defaults to `true`. 59 | 60 | #### `dereference` (boolean) 61 | 62 | If set to `true`, all local and remote references (http/https and file) $refs will be dereferenced. Defaults to `false`. 63 | 64 | #### `convertUnreferencedDefinitions` (boolean) 65 | 66 | Defaults to true. 67 | 68 | If a schema had a definitions property (which is valid in JSONSchema), and only some of those entries are referenced, we'll still try and convert the remaining definitions to OpenAPI. If you do not want this behavior, set this to `false`. 69 | 70 | #### `dereferenceOptions` (object = $RefParser.Options) 71 | 72 | Options to pass to the dereferencer (@apidevtools/json-schema-ref-parser). To prevent circular references, pass `{ dereference: { circular: 'ignore' } }`. 73 | 74 | ## Command Line 75 | 76 | ```sh 77 | Usage: 78 | json-schema-to-openapi-schema [options] 79 | 80 | Commands: 81 | convert Converts JSON Schema Draft 04 to OpenAPI 3.0 Schema Object 82 | 83 | Options: 84 | -h, --help Show help for any command 85 | -v, --version Output the CLI version number 86 | -d, --dereference If set all local and remote references (http/https and file) $refs will be dereferenced 87 | ``` 88 | 89 | ## Why? 90 | 91 | OpenAPI is often described as an extension of JSON Schema, but both specs have changed over time and grown independently. OpenAPI v2 was based on JSON Schema draft v4 with a long list of deviations, but OpenAPI v3 shrank that list, upping their support to draft v4 and making the list of discrepancies shorter. This has been solved for OpenAPI v3.1, but for those using OpenAPI v3.0, you can use this tool to solve [the divergence](https://apisyouwonthate.com/blog/openapi-and-json-schema-divergence). 92 | 93 | ![Diagram showing data model (the objects, payload bodies, etc) and service model (endpoints, headers, metadata, etc)](https://cdn-images-1.medium.com/max/1600/0*hijIL-3Xa5EFZ783.png) 94 | 95 | This tool sets out to allow folks to convert from JSON Schema (their one source of truth for everything) to OpenAPI (a thing for HTML docs and making SDKs). 96 | 97 | ## Versions 98 | 99 | - **From:** [JSON Schema Draft v5 †](http://json-schema.org/specification-links.html#draft-5) 100 | - **To:** [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) 101 | 102 | _† Draft v5 is also known as Draft Wright 00, as the drafts are often named after the author, and this was the first one by A. Wright. Amongst other things, draft v5 aimed to rewrite the meta files, but the experiment failed, meaning we need to continue to use the draft v4 metafiles. Ugh._ 103 | 104 | ## Converting Back 105 | 106 | To convert the other way, check out [openapi-schema-to-json-schema], which this package was based on. 107 | 108 | ## Tests 109 | 110 | To run the test-suite: 111 | 112 | ```shell 113 | npm test 114 | ``` 115 | 116 | ## Treeware 117 | 118 | This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/{venfor}/{package}) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats. 119 | 120 | ## Thanks 121 | 122 | - [Stoplight][] for [donating time and effort](https://stoplight.io/blog/companies-supporting-open-source/) to this project, and many more. 123 | - [mikunn][] for creating [openapi-schema-to-json-schema] which this is based on. 124 | - [Phil Sturgeon][] for flipping that conversion script about face. 125 | - [WeWork][] for giving this a home for a while. 126 | - [All Contributors][link-contributors] 127 | 128 | [mikunn]: https://github.com/mikunn 129 | [wework]: https://github.com/wework 130 | [stoplight]: https://stoplight.io/ 131 | [phil sturgeon]: https://github.com/philsturgeon 132 | [openapi-schema-to-json-schema]: https://github.com/openapi-contrib/openapi-schema-to-json-schema 133 | [link-contributors]: https://github.com/openapi-contrib/json-schema-to-openapi-schema/graphs/contributors 134 | -------------------------------------------------------------------------------- /bin/help-text.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": [ 3 | "Usage:", 4 | " json-schema-to-openapi-schema [options] ", 5 | "", 6 | "Commands:", 7 | " convert Converts JSON Schema Draft 04 to OpenAPI 3.0 Schema Object", 8 | "", 9 | "Options:", 10 | " -h, --help Show help for any command", 11 | " -v, --version Output the CLI version number", 12 | " -d, --dereference If set all local and remote references (http/https and file) $refs will be dereferenced", 13 | "" 14 | ], 15 | "convert": [ 16 | "Converts JSON Schema Draft 04 to OpenAPI 3.0 Schema Object.", 17 | "Returns a non-zero exit code if conversion fails.", 18 | "", 19 | "Usage:", 20 | " json-schema-to-openapi-schema convert [options] ", 21 | "", 22 | "Options:", 23 | " -d, --dereference If set all local and remote references (http/https and file) $refs will be dereferenced", 24 | "" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /bin/json-schema-to-openapi-schema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const yargs = require('yargs'); 5 | const converter = require('../dist/index.js').default; 6 | const helpText = require('./help-text.json'); 7 | const fs = require('fs'); 8 | const readFileAsync = require('util').promisify(fs.readFile); 9 | 10 | (async function main() { 11 | let args = parseArgs(); 12 | let command = args.command; 13 | let file = args.file; 14 | let options = args.options; 15 | 16 | if (options.help) { 17 | // Show help text and exit 18 | console.log(getHelpText(command)); 19 | process.exit(0); 20 | } else if (command === 'convert' && file) { 21 | // Convert the JSON Schema file 22 | await convert(file, options); 23 | } else { 24 | // Invalid args. Show help text and exit with non-zero 25 | console.error('Error: Invalid arguments\n'); 26 | console.error(getHelpText(command)); 27 | process.exit(1); 28 | } 29 | })(); 30 | 31 | /** 32 | * Parses the command-line arguments 33 | * 34 | * @returns {object} - The parsed arguments 35 | */ 36 | function parseArgs() { 37 | // Configure the argument parser 38 | yargs 39 | .option('d', { 40 | alias: 'dereference', 41 | type: 'boolean', 42 | default: false, 43 | }) 44 | .option('h', { 45 | alias: 'help', 46 | type: 'boolean', 47 | }); 48 | 49 | // Show the version number on "--version" or "-v" 50 | yargs.version().alias('v', 'version'); 51 | 52 | // Disable the default "--help" behavior 53 | yargs.help(false); 54 | 55 | // Parse the command-line arguments 56 | let args = yargs.argv; 57 | 58 | // Normalize the parsed arguments 59 | let parsed = { 60 | command: args._[0], 61 | file: args._[1], 62 | options: { 63 | dereference: args.dereference, 64 | help: args.help, 65 | }, 66 | }; 67 | 68 | return parsed; 69 | } 70 | 71 | /** 72 | * Convert an JSON Schema to OpenAPI schema 73 | * 74 | * @param {string} file - The path of the file to convert 75 | * @param {object} options - Conversion options 76 | */ 77 | async function convert(file, options) { 78 | try { 79 | const schema = await readFileAsync(file, 'utf8'); 80 | const converted = await converter(JSON.parse(schema), options); 81 | console.log(JSON.stringify(converted)); 82 | } catch (error) { 83 | errorHandler(error); 84 | } 85 | } 86 | 87 | /** 88 | * Returns the help text for the specified command 89 | * 90 | * @param {string} [commandName] - The command to show help text for 91 | * @returns {string} - the help text 92 | */ 93 | function getHelpText(commandName) { 94 | let lines = helpText[commandName] || helpText.default; 95 | return lines.join('\n'); 96 | } 97 | 98 | /** 99 | * Writes error information to stderr and exits with a non-zero code 100 | * 101 | * @param {Error} err 102 | */ 103 | function errorHandler(err) { 104 | let errorMessage = process.env.DEBUG ? err.stack : err.message; 105 | console.error(errorMessage); 106 | process.exit(1); 107 | } 108 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import prettierPlugin from 'eslint-plugin-prettier'; 3 | import unusedImportsPlugin from 'eslint-plugin-unused-imports'; 4 | 5 | import prettierExtends from 'eslint-config-prettier'; 6 | import { fixupPluginRules } from '@eslint/compat'; 7 | import globals from 'globals'; 8 | import tseslint from 'typescript-eslint'; 9 | 10 | const globalToUse = { 11 | ...globals.browser, 12 | ...globals.serviceworker, 13 | ...globals.es2021, 14 | ...globals.worker, 15 | ...globals.node, 16 | }; 17 | 18 | export default tseslint.config({ 19 | extends: [ 20 | { 21 | ignores: ['dist/**', 'bin/**'], 22 | }, 23 | prettierExtends, 24 | eslint.configs.recommended, 25 | ...tseslint.configs.recommended, 26 | ], 27 | plugins: { 28 | prettierPlugin, 29 | 'unused-imports': fixupPluginRules(unusedImportsPlugin), 30 | }, 31 | rules: { 32 | indent: [ 33 | 'error', 34 | 'tab', 35 | { 36 | SwitchCase: 1, 37 | }, 38 | ], 39 | 'linebreak-style': [ 40 | 'error', 41 | process.platform === 'win32' ? 'windows' : 'unix', 42 | ], 43 | quotes: ['error', 'single'], 44 | semi: ['error', 'always'], 45 | '@typescript-eslint/ban-ts-comment': 'off', 46 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 47 | '@typescript-eslint/consistent-type-imports': [ 48 | 'error', 49 | { 50 | prefer: 'type-imports', 51 | }, 52 | ], 53 | '@typescript-eslint/no-explicit-any': 'off', 54 | }, 55 | languageOptions: { 56 | globals: globalToUse, 57 | parserOptions: { 58 | ecmaFeatures: { 59 | jsx: true, 60 | }, 61 | }, 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openapi-contrib/json-schema-to-openapi-schema", 3 | "version": "0.0.0-development", 4 | "description": "Converts a JSON Schema to OpenAPI Schema Object", 5 | "files": [ 6 | "bin", 7 | "dist", 8 | "CHANGELOG.md", 9 | "LICENSE", 10 | "package.json" 11 | ], 12 | "exports": { 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "require": "./dist/index.js", 16 | "import": "./dist/index.mjs" 17 | } 18 | }, 19 | "bin": "bin/json-schema-to-openapi-schema.js", 20 | "types": "dist/index.d.ts", 21 | "main": "dist/index.js", 22 | "module": "dist/index.mjs", 23 | "scripts": { 24 | "prepublish": "yarn build", 25 | "build": "rimraf dist && tsup src/index.ts --format esm,cjs --dts --clean", 26 | "lint": "eslint . && prettier -c src", 27 | "lint:fix": "eslint . --fix && prettier -c src -w", 28 | "typecheck": "tsc --noEmit", 29 | "test": "vitest", 30 | "coverage": "vitest --coverage" 31 | }, 32 | "repository": "github:openapi-contrib/json-schema-to-openapi-schema", 33 | "author": "OpenAPI Contrib", 34 | "license": "MIT", 35 | "engines": { 36 | "node": ">=18" 37 | }, 38 | "dependencies": { 39 | "@apidevtools/json-schema-ref-parser": "^13.0.0", 40 | "json-schema-walker": "^3.0.1", 41 | "openapi-types": "^12.1.3", 42 | "yargs": "^18.0.0" 43 | }, 44 | "devDependencies": { 45 | "@eslint/compat": "^1.2.9", 46 | "@eslint/js": "^9.28.0", 47 | "@types/json-schema": "^7.0.15", 48 | "c8": "^10.1.3", 49 | "eslint": "^9.28.0", 50 | "eslint-config-prettier": "^10.1.5", 51 | "eslint-plugin-prettier": "^5.4.1", 52 | "eslint-plugin-unused-imports": "^4.1.4", 53 | "globals": "^16.2.0", 54 | "nock": "^14.0.5", 55 | "prettier": "^3.5.3", 56 | "rimraf": "^6.0.1", 57 | "tsup": "^8.5.0", 58 | "typescript": "^5.8.3", 59 | "typescript-eslint": "^8.33.1", 60 | "vitest": "^3.2.1" 61 | }, 62 | "prettier": { 63 | "singleQuote": true, 64 | "useTabs": true 65 | }, 66 | "packageManager": "yarn@4.9.1" 67 | } 68 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | // TODO: having definitions inside an oas3 schema isn't exactly valid, 2 | // maybe it is an idea to extract and split them into multiple oas3 schemas and reference to them. 3 | // For now leaving as is. 4 | export const allowedKeywords = [ 5 | '$ref', 6 | 'definitions', 7 | // From Schema 8 | 'title', 9 | 'multipleOf', 10 | 'maximum', 11 | 'exclusiveMaximum', 12 | 'minimum', 13 | 'exclusiveMinimum', 14 | 'maxLength', 15 | 'minLength', 16 | 'pattern', 17 | 'maxItems', 18 | 'minItems', 19 | 'uniqueItems', 20 | 'maxProperties', 21 | 'minProperties', 22 | 'required', 23 | 'enum', 24 | 'type', 25 | 'not', 26 | 'allOf', 27 | 'oneOf', 28 | 'anyOf', 29 | 'items', 30 | 'properties', 31 | 'additionalProperties', 32 | 'description', 33 | 'format', 34 | 'default', 35 | 'nullable', 36 | 'discriminator', 37 | 'readOnly', 38 | 'writeOnly', 39 | 'example', 40 | 'externalDocs', 41 | 'deprecated', 42 | 'xml', 43 | ]; 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | JSONSchema4, 3 | JSONSchema6Definition, 4 | JSONSchema7Definition, 5 | } from 'json-schema'; 6 | import type { Options, SchemaType, SchemaTypeKeys } from './types'; 7 | import { Walker } from 'json-schema-walker'; 8 | import { allowedKeywords } from './const'; 9 | import type { OpenAPIV3 } from 'openapi-types'; 10 | import type { JSONSchema } from '@apidevtools/json-schema-ref-parser'; 11 | 12 | class InvalidTypeError extends Error { 13 | constructor(message: string) { 14 | super(message); 15 | this.name = 'InvalidTypeError'; 16 | this.message = message; 17 | } 18 | } 19 | 20 | const oasExtensionPrefix = 'x-'; 21 | 22 | const handleDefinition = async ( 23 | def: JSONSchema7Definition | JSONSchema6Definition | JSONSchema4, 24 | schema: T, 25 | ) => { 26 | if (typeof def !== 'object') { 27 | return def; 28 | } 29 | 30 | const type = def.type; 31 | if (type) { 32 | // Walk just the definitions types 33 | const walker = new Walker(); 34 | await walker.loadSchema( 35 | { 36 | definitions: schema['definitions'] || [], 37 | ...def, 38 | $schema: schema['$schema'], 39 | } as any, 40 | { 41 | dereference: true, 42 | cloneSchema: true, 43 | dereferenceOptions: { 44 | dereference: { 45 | circular: 'ignore', 46 | }, 47 | }, 48 | }, 49 | ); 50 | await walker.walk(convertSchema, walker.vocabularies.DRAFT_07); 51 | if ('definitions' in walker.rootSchema) { 52 | delete (walker.rootSchema).definitions; 53 | } 54 | return walker.rootSchema; 55 | } 56 | if (Array.isArray(def)) { 57 | // if it's an array, we might want to reconstruct the type; 58 | const typeArr = def; 59 | const hasNull = typeArr.includes('null'); 60 | if (hasNull) { 61 | const actualTypes = typeArr.filter((l) => l !== 'null'); 62 | return { 63 | type: actualTypes.length === 1 ? actualTypes[0] : actualTypes, 64 | nullable: true, 65 | // this is incorrect but thats ok, we are in the inbetween phase here 66 | } as JSONSchema7Definition | JSONSchema6Definition | JSONSchema4; 67 | } 68 | } 69 | 70 | return def; 71 | }; 72 | 73 | export const convert = async ( 74 | schema: T, 75 | options?: Options, 76 | ): Promise => { 77 | const walker = new Walker(); 78 | const convertDefs = options?.convertUnreferencedDefinitions ?? true; 79 | await walker.loadSchema(schema, options); 80 | await walker.walk(convertSchema, walker.vocabularies.DRAFT_07); 81 | // if we want to convert unreferenced definitions, we need to do it iteratively here 82 | const rootSchema = walker.rootSchema as unknown as JSONSchema; 83 | if (convertDefs && rootSchema?.definitions) { 84 | for (const defName in rootSchema.definitions) { 85 | const def = rootSchema.definitions[defName]; 86 | rootSchema.definitions[defName] = await handleDefinition(def, schema); 87 | } 88 | } 89 | return rootSchema as OpenAPIV3.Document; 90 | }; 91 | 92 | function stripIllegalKeywords(schema: SchemaType) { 93 | if (typeof schema !== 'object') { 94 | return schema; 95 | } 96 | delete schema['$schema']; 97 | delete schema['$id']; 98 | if ('id' in schema) { 99 | delete schema['id']; 100 | } 101 | return schema; 102 | } 103 | 104 | function convertSchema(schema: SchemaType | undefined) { 105 | if (!schema) { 106 | return schema; 107 | } 108 | schema = stripIllegalKeywords(schema); 109 | schema = convertTypes(schema); 110 | schema = rewriteConst(schema); 111 | schema = convertDependencies(schema); 112 | schema = convertNullable(schema); 113 | schema = rewriteIfThenElse(schema); 114 | schema = rewriteExclusiveMinMax(schema); 115 | schema = convertExamples(schema); 116 | 117 | if (typeof schema['patternProperties'] === 'object') { 118 | schema = convertPatternProperties(schema); 119 | } 120 | 121 | if (schema.type === 'array' && typeof schema.items === 'undefined') { 122 | schema.items = {}; 123 | } 124 | 125 | // should be called last 126 | schema = convertIllegalKeywordsAsExtensions(schema); 127 | return schema; 128 | } 129 | const validTypes = new Set([ 130 | 'null', 131 | 'boolean', 132 | 'object', 133 | 'array', 134 | 'number', 135 | 'string', 136 | 'integer', 137 | ]); 138 | function validateType(type: any) { 139 | if (typeof type === 'object' && !Array.isArray(type)) { 140 | // Refs are allowed because they fix circular references 141 | if (type.$ref) { 142 | return; 143 | } 144 | // this is a de-referenced circular ref 145 | if (type.properties) { 146 | return; 147 | } 148 | } 149 | const types = Array.isArray(type) ? type : [type]; 150 | types.forEach((type) => { 151 | if (type && !validTypes.has(type)) 152 | throw new InvalidTypeError('Type "' + type + '" is not a valid type'); 153 | }); 154 | } 155 | 156 | function convertDependencies(schema: SchemaType) { 157 | const deps = schema.dependencies; 158 | if (typeof deps !== 'object') { 159 | return schema; 160 | } 161 | 162 | // Turns the dependencies keyword into an allOf of oneOf's 163 | // "dependencies": { 164 | // "post-office-box": ["street-address"] 165 | // }, 166 | // 167 | // becomes 168 | // 169 | // "allOf": [ 170 | // { 171 | // "oneOf": [ 172 | // {"not": {"required": ["post-office-box"]}}, 173 | // {"required": ["post-office-box", "street-address"]} 174 | // ] 175 | // } 176 | // 177 | 178 | delete schema['dependencies']; 179 | if (!Array.isArray(schema.allOf)) { 180 | schema.allOf = []; 181 | } 182 | 183 | for (const key in deps) { 184 | const foo: (JSONSchema4 & JSONSchema6Definition) & JSONSchema7Definition = { 185 | oneOf: [ 186 | { 187 | not: { 188 | required: [key], 189 | }, 190 | }, 191 | { 192 | required: [key, deps[key]].flat() as string[], 193 | }, 194 | ], 195 | }; 196 | schema.allOf.push(foo); 197 | } 198 | return schema; 199 | } 200 | 201 | function convertNullable(schema: SchemaType) { 202 | for (const key of ['oneOf', 'anyOf'] as const) { 203 | const schemas = schema[key] as JSONSchema4[]; 204 | if (!schemas) continue; 205 | 206 | if (!Array.isArray(schemas)) { 207 | return schema; 208 | } 209 | 210 | const hasNullable = schemas.some((item) => item.type === 'null'); 211 | 212 | if (!hasNullable) { 213 | return schema; 214 | } 215 | 216 | const filtered = schemas.filter((l) => l.type !== 'null'); 217 | for (const schemaEntry of filtered) { 218 | schemaEntry.nullable = true; 219 | } 220 | 221 | schema[key] = filtered; 222 | } 223 | 224 | return schema; 225 | } 226 | 227 | function convertTypes(schema: SchemaType) { 228 | if (typeof schema !== 'object') { 229 | return schema; 230 | } 231 | if (schema.type === undefined) { 232 | return schema; 233 | } 234 | 235 | validateType(schema.type); 236 | 237 | if (Array.isArray(schema.type)) { 238 | if (schema.type.includes('null')) { 239 | schema.nullable = true; 240 | } 241 | const typesWithoutNull = schema.type.filter((type) => type !== 'null'); 242 | if (typesWithoutNull.length === 0) { 243 | delete schema.type; 244 | } else if (typesWithoutNull.length === 1) { 245 | schema.type = typesWithoutNull[0]; 246 | } else { 247 | delete schema.type; 248 | schema.anyOf = typesWithoutNull.map((type) => ({ type })); 249 | } 250 | } else if (schema.type === 'null') { 251 | delete schema.type; 252 | schema.nullable = true; 253 | } 254 | 255 | return schema; 256 | } 257 | 258 | // "patternProperties did not make it into OpenAPI v3.0" 259 | // https://github.com/OAI/OpenAPI-Specification/issues/687 260 | function convertPatternProperties(schema: SchemaType) { 261 | schema['x-patternProperties'] = schema['patternProperties']; 262 | delete schema['patternProperties']; 263 | schema.additionalProperties ??= true; 264 | return schema; 265 | } 266 | 267 | // keywords (or property names) that are not recognized within OAS3 are rewritten into extensions. 268 | function convertIllegalKeywordsAsExtensions(schema: SchemaType) { 269 | const keys = Object.keys(schema) as SchemaTypeKeys[]; 270 | keys 271 | .filter( 272 | (keyword) => 273 | !keyword.startsWith(oasExtensionPrefix) && 274 | !allowedKeywords.includes(keyword), 275 | ) 276 | .forEach((keyword: SchemaTypeKeys) => { 277 | const key = `${oasExtensionPrefix}${keyword}` as keyof SchemaType; 278 | schema[key] = schema[keyword]; 279 | delete schema[keyword]; 280 | }); 281 | return schema; 282 | } 283 | 284 | function convertExamples(schema: SchemaType) { 285 | if (schema['examples'] && Array.isArray(schema['examples'])) { 286 | schema['example'] = schema['examples'][0]; 287 | delete schema['examples']; 288 | } 289 | 290 | return schema; 291 | } 292 | 293 | function rewriteConst(schema: SchemaType) { 294 | if (Object.hasOwnProperty.call(schema, 'const')) { 295 | schema.enum = [schema.const]; 296 | delete schema.const; 297 | } 298 | return schema; 299 | } 300 | 301 | function rewriteIfThenElse(schema: SchemaType) { 302 | if (typeof schema !== 'object') { 303 | return schema; 304 | } 305 | /* @handrews https://github.com/OAI/OpenAPI-Specification/pull/1766#issuecomment-442652805 306 | if and the *Of keywords 307 | 308 | There is a really easy solution for implementations, which is that 309 | 310 | if: X, then: Y, else: Z 311 | 312 | is equivalent to 313 | 314 | oneOf: [allOf: [X, Y], allOf: [not: X, Z]] 315 | */ 316 | if ('if' in schema && schema.if && schema.then) { 317 | schema.oneOf = [ 318 | { allOf: [schema.if, schema.then].filter(Boolean) }, 319 | { allOf: [{ not: schema.if }, schema.else].filter(Boolean) }, 320 | ]; 321 | delete schema.if; 322 | delete schema.then; 323 | delete schema.else; 324 | } 325 | return schema; 326 | } 327 | 328 | function rewriteExclusiveMinMax(schema: SchemaType) { 329 | if (typeof schema.exclusiveMaximum === 'number') { 330 | schema.maximum = schema.exclusiveMaximum; 331 | (schema as JSONSchema4).exclusiveMaximum = true; 332 | } 333 | if (typeof schema.exclusiveMinimum === 'number') { 334 | schema.minimum = schema.exclusiveMinimum; 335 | (schema as JSONSchema4).exclusiveMinimum = true; 336 | } 337 | return schema; 338 | } 339 | 340 | export default convert; 341 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema } from '@apidevtools/json-schema-ref-parser'; 2 | import type { ParserOptions } from '@apidevtools/json-schema-ref-parser'; 3 | 4 | export type addPrefixToObject = { 5 | [K in keyof JSONSchema as `x-${K}`]: JSONSchema[K]; 6 | }; 7 | 8 | export interface Options { 9 | cloneSchema?: boolean; 10 | dereference?: boolean; 11 | convertUnreferencedDefinitions?: boolean; 12 | dereferenceOptions?: ParserOptions | undefined; 13 | } 14 | type ExtendedJSONSchema = addPrefixToObject & JSONSchema; 15 | export type SchemaType = ExtendedJSONSchema & { 16 | example?: JSONSchema['examples'][number]; 17 | 'x-patternProperties'?: JSONSchema['patternProperties']; 18 | nullable?: boolean; 19 | }; 20 | export type SchemaTypeKeys = keyof SchemaType; 21 | -------------------------------------------------------------------------------- /test/__snapshots__/circular_schema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`converting circular/openapi.json without circular references turned off 1`] = ` 4 | { 5 | "definitions": { 6 | "child": { 7 | "properties": { 8 | "name": { 9 | "type": "string", 10 | }, 11 | "parents": { 12 | "items": { 13 | "properties": { 14 | "children": { 15 | "items": [Circular], 16 | "type": "array", 17 | }, 18 | "name": { 19 | "type": "string", 20 | }, 21 | }, 22 | }, 23 | "type": "array", 24 | }, 25 | }, 26 | }, 27 | "parent": { 28 | "properties": { 29 | "children": { 30 | "items": { 31 | "properties": { 32 | "name": { 33 | "type": "string", 34 | }, 35 | "parents": { 36 | "items": [Circular], 37 | "type": "array", 38 | }, 39 | }, 40 | }, 41 | "type": "array", 42 | }, 43 | "name": { 44 | "type": "string", 45 | }, 46 | }, 47 | }, 48 | "person": { 49 | "properties": { 50 | "name": { 51 | "type": "string", 52 | }, 53 | "spouse": { 54 | "type": [Circular], 55 | }, 56 | }, 57 | }, 58 | "thing": { 59 | "$ref": "#/definitions/thing", 60 | }, 61 | }, 62 | } 63 | `; 64 | -------------------------------------------------------------------------------- /test/__snapshots__/dereference_schema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`throws an error when dereferecing fails 1`] = ` 4 | { 5 | "additionalProperties": false, 6 | "definitions": { 7 | "configvariable": { 8 | "additionalProperties": false, 9 | "properties": { 10 | "default": { 11 | "type": "string", 12 | }, 13 | "name": { 14 | "pattern": "^[A-Z_]+[A-Z0-9_]*$", 15 | "type": "string", 16 | }, 17 | "required": { 18 | "default": true, 19 | "type": "boolean", 20 | }, 21 | }, 22 | "required": [ 23 | "name", 24 | ], 25 | "type": "object", 26 | }, 27 | "envVarName": { 28 | "pattern": "^[A-Z_]+[A-Z0-9_]*$", 29 | "type": "string", 30 | }, 31 | }, 32 | "properties": { 33 | "componentId": { 34 | "pattern": "^(.*)$", 35 | "title": "The component id Schema", 36 | "type": "string", 37 | }, 38 | "configurationTemplate": { 39 | "items": { 40 | "$ref": "#/definitions/configvariable", 41 | }, 42 | "title": "The Configurationtemplate Schema", 43 | "type": "array", 44 | }, 45 | }, 46 | "required": [ 47 | "componentId", 48 | ], 49 | "title": "Component Manifest Schema", 50 | "type": "object", 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /test/array-items.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('array-items', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'array', 7 | }; 8 | 9 | const result = await convert(schema); 10 | 11 | const expected = { 12 | type: 'array', 13 | items: {}, 14 | }; 15 | 16 | expect(result).toEqual(expected); 17 | }); 18 | -------------------------------------------------------------------------------- /test/circular_schema.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | import { getSchema } from './helpers'; 3 | const test = 'circular'; 4 | 5 | it(`converts ${test}/openapi.json`, async ({ expect }) => { 6 | const schema = getSchema(test + '/json-schema.json'); 7 | const result = await convert(schema, { 8 | dereference: true, 9 | dereferenceOptions: { dereference: { circular: 'ignore' } }, 10 | }); 11 | const expected = getSchema(test + '/openapi.json'); 12 | expect(result).toEqual(expected); 13 | }); 14 | 15 | it(`converting ${test}/openapi.json in place`, async ({ expect }) => { 16 | const schema = getSchema(test + '/json-schema.json'); 17 | const result = await convert(schema, { 18 | cloneSchema: false, 19 | dereference: true, 20 | dereferenceOptions: { dereference: { circular: 'ignore' } }, 21 | }); 22 | const expected = getSchema(test + '/openapi.json'); 23 | expect(schema).toEqual(result); 24 | expect(result).toEqual(expected); 25 | }); 26 | 27 | it(`converting ${test}/openapi.json without circular references turned off `, async ({ 28 | expect, 29 | }) => { 30 | const schema = getSchema(test + '/json-schema.json'); 31 | const result = await convert(schema, { 32 | cloneSchema: false, 33 | dereference: true, 34 | }); 35 | expect(result).toMatchSnapshot(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/clone_schema.test.ts: -------------------------------------------------------------------------------- 1 | import { convert } from '../src'; 2 | 3 | it('cloning schema by default', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: ['string', 'null'], 7 | }; 8 | 9 | const result = await convert(schema); 10 | 11 | const expected = { 12 | type: 'string', 13 | nullable: true, 14 | }; 15 | 16 | expect(result).toEqual(expected); 17 | expect(result).not.toEqual(schema); 18 | }); 19 | 20 | it('cloning schema with cloneSchema option', async ({ expect }) => { 21 | const schema = { 22 | $schema: 'http://json-schema.org/draft-04/schema#', 23 | type: ['string', 'null'], 24 | }; 25 | 26 | const result = await convert(schema, { cloneSchema: true }); 27 | 28 | const expected = { 29 | type: 'string', 30 | nullable: true, 31 | }; 32 | 33 | expect(result).toEqual(expected); 34 | expect(result).not.toEqual(schema); 35 | }); 36 | 37 | it('direct schema modification', async ({ expect }) => { 38 | const schema = { 39 | $schema: 'http://json-schema.org/draft-04/schema#', 40 | type: ['string', 'null'], 41 | }; 42 | 43 | const result = await convert(schema, { cloneSchema: false }); 44 | 45 | const expected = { 46 | type: 'string', 47 | nullable: true, 48 | }; 49 | 50 | expect(result).toEqual(expected); 51 | expect(result).toEqual(schema); 52 | }); 53 | -------------------------------------------------------------------------------- /test/combination_keywords.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('iterates allOfs and converts types', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | allOf: [ 7 | { 8 | type: 'object', 9 | required: ['foo'], 10 | properties: { 11 | foo: { 12 | type: 'integer', 13 | format: 'int64', 14 | }, 15 | }, 16 | }, 17 | { 18 | allOf: [ 19 | { 20 | type: 'number', 21 | format: 'double', 22 | }, 23 | ], 24 | }, 25 | ], 26 | }; 27 | 28 | const result = await convert(schema); 29 | 30 | const expected = { 31 | allOf: [ 32 | { 33 | type: 'object', 34 | required: ['foo'], 35 | properties: { 36 | foo: { 37 | type: 'integer', 38 | format: 'int64', 39 | }, 40 | }, 41 | }, 42 | { 43 | allOf: [ 44 | { 45 | type: 'number', 46 | format: 'double', 47 | }, 48 | ], 49 | }, 50 | ], 51 | }; 52 | 53 | expect(result).toEqual(expected); 54 | }); 55 | 56 | it('iterates anyOfs and converts types', async ({ expect }) => { 57 | const schema = { 58 | $schema: 'http://json-schema.org/draft-04/schema#', 59 | anyOf: [ 60 | { 61 | type: 'object', 62 | required: ['foo'], 63 | properties: { 64 | foo: { 65 | type: 'integer', 66 | format: 'int64', 67 | }, 68 | }, 69 | }, 70 | { 71 | anyOf: [ 72 | { 73 | type: 'object', 74 | properties: { 75 | bar: { 76 | type: 'number', 77 | format: 'double', 78 | }, 79 | }, 80 | }, 81 | ], 82 | }, 83 | ], 84 | }; 85 | 86 | const result = await convert(schema); 87 | 88 | const expected = { 89 | anyOf: [ 90 | { 91 | type: 'object', 92 | required: ['foo'], 93 | properties: { 94 | foo: { 95 | type: 'integer', 96 | format: 'int64', 97 | }, 98 | }, 99 | }, 100 | { 101 | anyOf: [ 102 | { 103 | type: 'object', 104 | properties: { 105 | bar: { 106 | type: 'number', 107 | format: 'double', 108 | }, 109 | }, 110 | }, 111 | ], 112 | }, 113 | ], 114 | }; 115 | 116 | expect(result).toEqual(expected); 117 | }); 118 | 119 | it('iterates oneOfs and converts types', async ({ expect }) => { 120 | const schema = { 121 | $schema: 'http://json-schema.org/draft-04/schema#', 122 | oneOf: [ 123 | { 124 | type: 'object', 125 | required: ['foo'], 126 | properties: { 127 | foo: { 128 | type: ['string', 'null'], 129 | }, 130 | }, 131 | }, 132 | { 133 | oneOf: [ 134 | { 135 | type: 'object', 136 | properties: { 137 | bar: { 138 | type: ['string', 'null'], 139 | }, 140 | }, 141 | }, 142 | ], 143 | }, 144 | ], 145 | }; 146 | 147 | const result = await convert(schema); 148 | 149 | const expected = { 150 | oneOf: [ 151 | { 152 | type: 'object', 153 | required: ['foo'], 154 | properties: { 155 | foo: { 156 | type: 'string', 157 | nullable: true, 158 | }, 159 | }, 160 | }, 161 | { 162 | oneOf: [ 163 | { 164 | type: 'object', 165 | properties: { 166 | bar: { 167 | type: 'string', 168 | nullable: true, 169 | }, 170 | }, 171 | }, 172 | ], 173 | }, 174 | ], 175 | }; 176 | 177 | expect(result).toEqual(expected); 178 | }); 179 | 180 | it('converts types in not', async ({ expect }) => { 181 | const schema = { 182 | $schema: 'http://json-schema.org/draft-04/schema#', 183 | type: 'object', 184 | properties: { 185 | not: { 186 | type: ['string', 'null'], 187 | minLength: 8, 188 | }, 189 | }, 190 | }; 191 | 192 | const result = await convert(schema); 193 | 194 | const expected = { 195 | type: 'object', 196 | properties: { 197 | not: { 198 | type: 'string', 199 | nullable: true, 200 | minLength: 8, 201 | }, 202 | }, 203 | }; 204 | 205 | expect(result).toEqual(expected); 206 | }); 207 | 208 | it('nested combination keywords', async ({ expect }) => { 209 | const schema = { 210 | $schema: 'http://json-schema.org/draft-04/schema#', 211 | anyOf: [ 212 | { 213 | allOf: [ 214 | { 215 | type: 'object', 216 | properties: { 217 | foo: { 218 | type: ['string', 'null'], 219 | }, 220 | }, 221 | }, 222 | { 223 | type: 'object', 224 | properties: { 225 | bar: { 226 | type: ['integer', 'null'], 227 | }, 228 | }, 229 | }, 230 | ], 231 | }, 232 | { 233 | type: 'object', 234 | properties: { 235 | foo: { 236 | type: 'string', 237 | }, 238 | }, 239 | }, 240 | { 241 | not: { 242 | type: 'string', 243 | example: 'foobar', 244 | }, 245 | }, 246 | ], 247 | }; 248 | 249 | const result = await convert(schema); 250 | 251 | const expected = { 252 | anyOf: [ 253 | { 254 | allOf: [ 255 | { 256 | type: 'object', 257 | properties: { 258 | foo: { 259 | type: 'string', 260 | nullable: true, 261 | }, 262 | }, 263 | }, 264 | { 265 | type: 'object', 266 | properties: { 267 | bar: { 268 | type: 'integer', 269 | nullable: true, 270 | }, 271 | }, 272 | }, 273 | ], 274 | }, 275 | { 276 | type: 'object', 277 | properties: { 278 | foo: { 279 | type: 'string', 280 | }, 281 | }, 282 | }, 283 | { 284 | not: { 285 | type: 'string', 286 | example: 'foobar', 287 | }, 288 | }, 289 | ], 290 | }; 291 | 292 | expect(result).toEqual(expected); 293 | }); 294 | -------------------------------------------------------------------------------- /test/complex_schemas.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | import { getSchema } from './helpers'; 3 | 4 | ['basic', 'address', 'calendar', 'events'].forEach((test) => { 5 | it(`converts ${test}/openapi.json`, async ({ expect }) => { 6 | const schema = getSchema(test + '/json-schema.json'); 7 | const result = await convert(schema); 8 | 9 | const expected = getSchema(test + '/openapi.json'); 10 | 11 | expect(result).toEqual(expected); 12 | }); 13 | 14 | it(`converting ${test}/openapi.json in place`, async ({ expect }) => { 15 | const schema = getSchema(test + '/json-schema.json'); 16 | const result = await convert(schema, { cloneSchema: false }); 17 | const expected = getSchema(test + '/openapi.json'); 18 | 19 | expect(schema).toEqual(result); 20 | expect(result).toEqual(expected); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/const.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('const', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'string', 7 | const: 'hello', 8 | }; 9 | 10 | const result = await convert(schema); 11 | 12 | const expected = { 13 | type: 'string', 14 | enum: ['hello'], 15 | }; 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | 20 | it('falsy const', async ({ expect }) => { 21 | const schema = { 22 | $schema: 'http://json-schema.org/draft-04/schema#', 23 | type: 'boolean', 24 | const: false, 25 | }; 26 | 27 | const result = await convert(schema); 28 | 29 | const expected = { 30 | type: 'boolean', 31 | enum: [false], 32 | }; 33 | 34 | expect(result).toEqual(expected); 35 | }); 36 | -------------------------------------------------------------------------------- /test/default-null.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('supports default values of null', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'object', 7 | properties: { 8 | nullableStringWithDefault: { 9 | default: null, 10 | oneOf: [{ type: 'string' }, { type: 'null' }], 11 | }, 12 | }, 13 | }; 14 | 15 | const result = await convert(schema); 16 | 17 | const expected = { 18 | type: 'object', 19 | properties: { 20 | nullableStringWithDefault: { 21 | default: null, 22 | oneOf: [{ type: 'string', nullable: true }], 23 | }, 24 | }, 25 | }; 26 | 27 | expect(result).toEqual(expected); 28 | }); 29 | -------------------------------------------------------------------------------- /test/dereference_schema.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | import { join } from 'path'; 3 | import nock from 'nock'; 4 | import * as path from 'path'; 5 | 6 | it('not dereferencing schema by default', async ({ expect }) => { 7 | const schema = { 8 | $schema: 'http://json-schema.org/draft-04/schema#', 9 | properties: { 10 | foo: { 11 | $ref: '#/definitions/foo', 12 | }, 13 | }, 14 | definitions: { 15 | foo: ['string', 'null'], 16 | }, 17 | }; 18 | 19 | const result = await convert(JSON.parse(JSON.stringify(schema))); 20 | 21 | const expected: any = { ...schema }; 22 | if ('$schema' in expected) { 23 | delete expected.$schema; 24 | } 25 | expected.definitions = { 26 | foo: { 27 | type: 'string', 28 | nullable: true, 29 | }, 30 | }; 31 | 32 | expect(result).toEqual(expected); 33 | }); 34 | 35 | it('dereferencing schema with deference option', async ({ expect }) => { 36 | const schema = { 37 | $schema: 'http://json-schema.org/draft-04/schema#', 38 | type: { 39 | $ref: '#/definitions/foo', 40 | }, 41 | definitions: { 42 | foo: ['string', 'null'], 43 | }, 44 | }; 45 | 46 | const result = await convert(schema, { dereference: true }); 47 | 48 | const expected = { 49 | type: 'string', 50 | nullable: true, 51 | definitions: { 52 | foo: { type: 'string', nullable: true }, 53 | }, 54 | }; 55 | 56 | expect(result).toEqual(expected); 57 | }); 58 | 59 | it('dereferencing schema with deference option at root', async ({ expect }) => { 60 | const schema = { 61 | definitions: { 62 | AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest: 63 | { 64 | type: 'object', 65 | additionalProperties: false, 66 | properties: { 67 | navigationMenuIdentity: { 68 | $ref: '#/definitions/NavigationMenuIdentity', 69 | }, 70 | sessionId: { 71 | type: 'string', 72 | }, 73 | }, 74 | required: [], 75 | title: 76 | 'AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest', 77 | }, 78 | NavigationMenuIdentity: { 79 | type: 'object', 80 | additionalProperties: false, 81 | properties: { 82 | Id: { 83 | type: 'string', 84 | }, 85 | LastModifiedDate: { 86 | type: 'string', 87 | }, 88 | Name: { 89 | type: 'string', 90 | }, 91 | __type: { 92 | type: 'string', 93 | }, 94 | }, 95 | required: [], 96 | title: 'NavigationMenuIdentity', 97 | }, 98 | }, 99 | $ref: '#/definitions/AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest', 100 | }; 101 | 102 | const result = await convert(schema, { dereference: true }); 103 | 104 | const expected = { 105 | type: 'object', 106 | additionalProperties: false, 107 | properties: { 108 | navigationMenuIdentity: { 109 | type: 'object', 110 | additionalProperties: false, 111 | properties: { 112 | Id: { 113 | type: 'string', 114 | }, 115 | LastModifiedDate: { 116 | type: 'string', 117 | }, 118 | Name: { 119 | type: 'string', 120 | }, 121 | __type: { 122 | type: 'string', 123 | }, 124 | }, 125 | required: [], 126 | title: 'NavigationMenuIdentity', 127 | }, 128 | sessionId: { 129 | type: 'string', 130 | }, 131 | }, 132 | required: [], 133 | title: 134 | 'AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest', 135 | definitions: { 136 | AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest: 137 | { 138 | type: 'object', 139 | additionalProperties: false, 140 | properties: { 141 | navigationMenuIdentity: { 142 | type: 'object', 143 | additionalProperties: false, 144 | properties: { 145 | Id: { 146 | type: 'string', 147 | }, 148 | LastModifiedDate: { 149 | type: 'string', 150 | }, 151 | Name: { 152 | type: 'string', 153 | }, 154 | __type: { 155 | type: 'string', 156 | }, 157 | }, 158 | required: [], 159 | title: 'NavigationMenuIdentity', 160 | }, 161 | sessionId: { 162 | type: 'string', 163 | }, 164 | }, 165 | required: [], 166 | title: 167 | 'AgilityServerWebServicesSDKServerServiceSVCJSONGetNavigationMenu2PostRequest', 168 | }, 169 | NavigationMenuIdentity: { 170 | type: 'object', 171 | additionalProperties: false, 172 | properties: { 173 | Id: { 174 | type: 'string', 175 | }, 176 | LastModifiedDate: { 177 | type: 'string', 178 | }, 179 | Name: { 180 | type: 'string', 181 | }, 182 | __type: { 183 | type: 'string', 184 | }, 185 | }, 186 | required: [], 187 | title: 'NavigationMenuIdentity', 188 | }, 189 | }, 190 | }; 191 | 192 | expect(result).toEqual(expected); 193 | }); 194 | 195 | // skip until nock supports native fetch https://github.com/nock/nock/issues/2397 196 | it.skip('dereferencing schema with remote http and https references', async ({ 197 | expect, 198 | }) => { 199 | nock('http://foo.bar/') 200 | .get('/schema.yaml') 201 | .replyWithFile(200, join(__dirname, 'fixtures/definitions.yaml'), { 202 | 'Content-Type': 'application/yaml', 203 | }); 204 | 205 | nock('https://baz.foo/') 206 | .get('/schema.yaml') 207 | .replyWithFile(200, join(__dirname, 'fixtures/definitions.yaml'), { 208 | 'Content-Type': 'application/yaml', 209 | }); 210 | 211 | const schema = { 212 | $schema: 'http://json-schema.org/draft-04/schema#', 213 | allOf: [ 214 | { $ref: 'http://foo.bar/schema.yaml#/definitions/foo' }, 215 | { $ref: 'https://baz.foo/schema.yaml#/definitions/bar' }, 216 | ], 217 | }; 218 | 219 | const result = await convert(schema, { dereference: true }); 220 | 221 | const expected = { 222 | allOf: [{ type: 'string' }, { type: 'number' }], 223 | }; 224 | 225 | expect(result).toEqual(expected); 226 | }); 227 | 228 | it('dereferencing schema with file references', async ({ expect }) => { 229 | const fileRef = join(__dirname, 'fixtures/definitions.yaml#/definitions/bar'); 230 | const unixStyle = path.resolve(fileRef).split(path.sep).join('/'); 231 | const schema = { 232 | $schema: 'http://json-schema.org/draft-04/schema#', 233 | allOf: [ 234 | // points to current working directory, hence the `test` prefix 235 | { $ref: './test/fixtures/definitions.yaml#/definitions/foo' }, 236 | { $ref: unixStyle }, 237 | ], 238 | }; 239 | 240 | const result = await convert(schema, { dereference: true }); 241 | 242 | const expected = { 243 | allOf: [{ type: 'string' }, { type: 'number' }], 244 | }; 245 | 246 | expect(result).toEqual(expected); 247 | }); 248 | 249 | it('throws an error when dereferecing fails', async ({ expect }) => { 250 | const schema = { 251 | $schema: 'http://json-schema.org/draft-04/schema#', 252 | properties: { 253 | foo: { 254 | $ref: './bad.json', 255 | }, 256 | }, 257 | }; 258 | 259 | let error; 260 | try { 261 | await convert(schema, { dereference: true }); 262 | } catch (e) { 263 | error = e; 264 | } 265 | 266 | expect(error).have.property('ioErrorCode', 'ENOENT'); 267 | }); 268 | 269 | it('throws an error when dereferecing fails', async ({ expect }) => { 270 | const schema = { 271 | definitions: { 272 | envVarName: { 273 | type: 'string', 274 | pattern: '^[A-Z_]+[A-Z0-9_]*$', 275 | }, 276 | configvariable: { 277 | type: 'object', 278 | properties: { 279 | name: { $ref: '#/definitions/envVarName' }, 280 | default: { type: 'string' }, 281 | required: { type: 'boolean', default: true }, 282 | }, 283 | required: ['name'], 284 | additionalProperties: false, 285 | }, 286 | }, 287 | $schema: 'http://json-schema.org/draft-07/schema#', 288 | $id: 'http://example.com/root.json', 289 | type: 'object', 290 | title: 'Component Manifest Schema', 291 | required: ['componentId'], 292 | additionalProperties: false, 293 | properties: { 294 | componentId: { 295 | $id: '#/properties/componentId', 296 | type: 'string', 297 | title: 'The component id Schema', 298 | pattern: '^(.*)$', 299 | }, 300 | configurationTemplate: { 301 | $id: '#/properties/configurationTemplate', 302 | type: 'array', 303 | title: 'The Configurationtemplate Schema', 304 | items: { 305 | $ref: '#/definitions/configvariable', 306 | }, 307 | }, 308 | }, 309 | }; 310 | 311 | const result = await convert(schema); 312 | 313 | expect(result).toMatchSnapshot(); 314 | }); 315 | -------------------------------------------------------------------------------- /test/examples.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('uses the first example from a schema', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-06/schema#', 6 | examples: ['foo', 'bar'], 7 | }; 8 | 9 | const result = await convert(schema); 10 | 11 | expect(result).toEqual({ 12 | example: 'foo', 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/exclusiveMinMax.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('exclusiveMinMax', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'integer', 7 | exclusiveMaximum: 10, 8 | exclusiveMinimum: 0, 9 | }; 10 | 11 | const result = await convert(schema); 12 | 13 | const expected = { 14 | type: 'integer', 15 | maximum: 10, 16 | exclusiveMaximum: true, 17 | minimum: 0, 18 | exclusiveMinimum: true, 19 | }; 20 | 21 | expect(result).toEqual(expected); 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/definitions.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | foo: 3 | type: 'string' 4 | bar: 5 | type: 'number' 6 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export const getSchema = (file: string) => { 5 | const path = join(__dirname, 'schemas', file); 6 | return JSON.parse(fs.readFileSync(path).toString()); 7 | }; 8 | -------------------------------------------------------------------------------- /test/if-then-else.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('if-then-else', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | if: { type: 'object' }, 7 | then: { properties: { id: { type: 'string' } } }, 8 | else: { format: 'uuid' }, 9 | }; 10 | 11 | const result = await convert(schema); 12 | 13 | const expected = { 14 | oneOf: [ 15 | { 16 | allOf: [{ type: 'object' }, { properties: { id: { type: 'string' } } }], 17 | }, 18 | { allOf: [{ not: { type: 'object' } }, { format: 'uuid' }] }, 19 | ], 20 | }; 21 | 22 | expect(result).toEqual(expected); 23 | }); 24 | 25 | it('if-then', async ({ expect }) => { 26 | const schema = { 27 | $schema: 'http://json-schema.org/draft-07/schema#', 28 | type: 'object', 29 | properties: { 30 | type: { 31 | type: 'string', 32 | enum: ['css', 'js', 'i18n', 'json'], 33 | }, 34 | locale: { 35 | type: 'string', 36 | }, 37 | }, 38 | if: { 39 | properties: { 40 | type: { 41 | const: 'i18n', 42 | }, 43 | }, 44 | }, 45 | then: { 46 | required: ['locale'], 47 | }, 48 | }; 49 | const result = await convert(schema); 50 | 51 | const expected = { 52 | type: 'object', 53 | properties: { 54 | type: { 55 | type: 'string', 56 | enum: ['css', 'js', 'i18n', 'json'], 57 | }, 58 | locale: { 59 | type: 'string', 60 | }, 61 | }, 62 | oneOf: [ 63 | { 64 | allOf: [ 65 | { 66 | properties: { 67 | type: { 68 | enum: ['i18n'], 69 | }, 70 | }, 71 | }, 72 | { 73 | required: ['locale'], 74 | }, 75 | ], 76 | }, 77 | { 78 | allOf: [ 79 | { 80 | not: { 81 | properties: { 82 | type: { 83 | enum: ['i18n'], 84 | }, 85 | }, 86 | }, 87 | }, 88 | ], 89 | }, 90 | ], 91 | }; 92 | 93 | expect(result).toEqual(expected); 94 | }); 95 | -------------------------------------------------------------------------------- /test/invalid_types.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | import { getSchema } from './helpers'; 3 | 4 | it('dateTime is invalid type', async ({ expect }) => { 5 | const schema = { type: 'dateTime' }; 6 | await expect(() => convert(schema)).rejects.toThrowError( 7 | /is not a valid type/, 8 | ); 9 | }); 10 | 11 | it('foo is invalid type', async ({ expect }) => { 12 | const schema = { type: 'foo' }; 13 | await expect(() => convert(schema)).rejects.toThrowError( 14 | /is not a valid type/, 15 | ); 16 | }); 17 | 18 | it('invalid type inside complex schema', async ({ expect }) => { 19 | const schema = getSchema('invalid/json-schema.json'); 20 | await expect(() => convert(schema)).rejects.toThrowError( 21 | /is not a valid type/, 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /test/items.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('items', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'array', 7 | items: { 8 | type: 'string', 9 | format: 'date-time', 10 | example: '2017-01-01T12:34:56Z', 11 | }, 12 | }; 13 | 14 | const result = await convert(schema); 15 | 16 | const expected = { 17 | type: 'array', 18 | items: { 19 | type: 'string', 20 | format: 'date-time', 21 | example: '2017-01-01T12:34:56Z', 22 | }, 23 | }; 24 | 25 | expect(result).toEqual(expected); 26 | }); 27 | -------------------------------------------------------------------------------- /test/nullable.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | import { describe } from 'vitest'; 3 | import type { JSONSchema4 } from 'json-schema'; 4 | 5 | describe('nullable', () => { 6 | it('adds `nullable: true` for `type: [string, null]`', async ({ expect }) => { 7 | const schema = { 8 | $schema: 'http://json-schema.org/draft-04/schema#', 9 | type: ['string', 'null'], 10 | } satisfies JSONSchema4; 11 | 12 | const result = await convert(schema); 13 | 14 | expect(result).toEqual({ 15 | type: 'string', 16 | nullable: true, 17 | }); 18 | }); 19 | 20 | it.each(['oneOf', 'anyOf'] as const)( 21 | 'supports nullables inside sub-schemas %s', 22 | async (key) => { 23 | const schema = { 24 | $schema: 'http://json-schema.org/draft-04/schema#', 25 | [key]: [{ type: 'string' }, { type: 'null' }], 26 | } satisfies JSONSchema4; 27 | 28 | const result = await convert(schema); 29 | 30 | expect(result).toEqual({ 31 | [key]: [{ type: 'string', nullable: true }], 32 | }); 33 | }, 34 | ); 35 | 36 | it('supports nullables inside definitions', async ({ expect }) => { 37 | const schema = { 38 | $schema: 'http://json-schema.org/draft-07/schema#', 39 | definitions: { 40 | Product: { 41 | type: 'object', 42 | properties: { 43 | name: { 44 | type: 'string', 45 | }, 46 | price: { 47 | type: 'number', 48 | }, 49 | rating: { 50 | type: ['null', 'number'], 51 | }, 52 | }, 53 | required: ['name', 'price', 'rating'], 54 | }, 55 | ProductList: { 56 | type: 'object', 57 | properties: { 58 | name: { 59 | type: 'string', 60 | }, 61 | version: { 62 | type: 'string', 63 | }, 64 | products: { 65 | type: 'array', 66 | items: { 67 | type: 'object', 68 | properties: { 69 | name: { 70 | type: 'string', 71 | }, 72 | price: { 73 | type: 'number', 74 | }, 75 | rating: { 76 | type: ['null', 'number'], 77 | }, 78 | }, 79 | required: ['name', 'price', 'rating'], 80 | }, 81 | }, 82 | }, 83 | required: ['name', 'products', 'version'], 84 | }, 85 | }, 86 | }; 87 | 88 | const result = await convert(schema); 89 | 90 | expect(result).toEqual({ 91 | definitions: { 92 | Product: { 93 | type: 'object', 94 | properties: { 95 | name: { 96 | type: 'string', 97 | }, 98 | price: { 99 | type: 'number', 100 | }, 101 | rating: { 102 | type: 'number', 103 | nullable: true, 104 | }, 105 | }, 106 | required: ['name', 'price', 'rating'], 107 | }, 108 | ProductList: { 109 | type: 'object', 110 | properties: { 111 | name: { 112 | type: 'string', 113 | }, 114 | version: { 115 | type: 'string', 116 | }, 117 | products: { 118 | type: 'array', 119 | items: { 120 | type: 'object', 121 | properties: { 122 | name: { 123 | type: 'string', 124 | }, 125 | price: { 126 | type: 'number', 127 | }, 128 | rating: { 129 | type: 'number', 130 | nullable: true, 131 | }, 132 | }, 133 | required: ['name', 'price', 'rating'], 134 | }, 135 | }, 136 | }, 137 | required: ['name', 'products', 'version'], 138 | }, 139 | }, 140 | }); 141 | }); 142 | 143 | it('does not add nullable for non null types', async ({ expect }) => { 144 | const schema = { 145 | $schema: 'http://json-schema.org/draft-04/schema#', 146 | type: 'string', 147 | } satisfies JSONSchema4; 148 | 149 | const result = await convert(schema); 150 | 151 | expect(result).toEqual({ 152 | type: 'string', 153 | }); 154 | }); 155 | 156 | it.each(['oneOf', 'anyOf'] as const)( 157 | 'adds nullable for types with null', 158 | async (key) => { 159 | const schema = { 160 | $schema: 'http://json-schema.org/draft-04/schema#', 161 | title: 'NullExample', 162 | description: 'Null Example', 163 | [key]: [ 164 | { 165 | type: 'object', 166 | properties: { 167 | foo: { 168 | type: 'string', 169 | }, 170 | }, 171 | }, 172 | { 173 | type: 'object', 174 | properties: { 175 | bar: { 176 | type: 'number', 177 | }, 178 | }, 179 | }, 180 | { 181 | type: 'null', 182 | }, 183 | ], 184 | } satisfies JSONSchema4; 185 | 186 | const result = await convert(schema); 187 | 188 | expect(result).toEqual({ 189 | title: 'NullExample', 190 | description: 'Null Example', 191 | [key]: [ 192 | { 193 | type: 'object', 194 | properties: { 195 | foo: { 196 | type: 'string', 197 | }, 198 | }, 199 | nullable: true, 200 | }, 201 | { 202 | type: 'object', 203 | properties: { 204 | bar: { 205 | type: 'number', 206 | }, 207 | }, 208 | nullable: true, 209 | }, 210 | ], 211 | }); 212 | }, 213 | ); 214 | }); 215 | -------------------------------------------------------------------------------- /test/pattern_properties.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('renames patternProperties to x-patternProperties', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'object', 7 | additionalProperties: { 8 | type: 'string', 9 | }, 10 | patternProperties: { 11 | '^[a-z]*$': { 12 | type: 'string', 13 | }, 14 | }, 15 | }; 16 | 17 | const result = await convert(schema); 18 | 19 | const expected = { 20 | type: 'object', 21 | additionalProperties: { 22 | type: 'string', 23 | }, 24 | 'x-patternProperties': { 25 | '^[a-z]*$': { 26 | type: 'string', 27 | }, 28 | }, 29 | }; 30 | 31 | expect(result).toEqual(expected); 32 | }); 33 | -------------------------------------------------------------------------------- /test/properties.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('type array', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: ['string', 'null'], 7 | }; 8 | 9 | const result = await convert(schema); 10 | 11 | const expected = { 12 | type: 'string', 13 | nullable: true, 14 | }; 15 | 16 | expect(result).toEqual(expected); 17 | }); 18 | 19 | it('properties', async ({ expect }) => { 20 | const schema = { 21 | $schema: 'http://json-schema.org/draft-04/schema#', 22 | type: 'object', 23 | required: ['bar'], 24 | properties: { 25 | foo: { 26 | type: 'string', 27 | }, 28 | bar: { 29 | type: ['string', 'null'], 30 | }, 31 | }, 32 | }; 33 | 34 | const result = await convert(schema); 35 | 36 | const expected = { 37 | type: 'object', 38 | required: ['bar'], 39 | properties: { 40 | foo: { 41 | type: 'string', 42 | }, 43 | bar: { 44 | type: 'string', 45 | nullable: true, 46 | }, 47 | }, 48 | }; 49 | 50 | expect(result).toEqual(expected); 51 | }); 52 | 53 | it('addionalProperties is false', async ({ expect }) => { 54 | const schema = { 55 | $schema: 'http://json-schema.org/draft-04/schema#', 56 | type: 'object', 57 | properties: { 58 | foo: { 59 | type: 'string', 60 | }, 61 | }, 62 | additionalProperties: false, 63 | }; 64 | 65 | const result = await convert(schema); 66 | 67 | const expected = { 68 | type: 'object', 69 | properties: { 70 | foo: { 71 | type: 'string', 72 | }, 73 | }, 74 | additionalProperties: false, 75 | }; 76 | 77 | expect(result).toEqual(expected); 78 | }); 79 | 80 | it('addionalProperties is true', async ({ expect }) => { 81 | const schema = { 82 | $schema: 'http://json-schema.org/draft-04/schema#', 83 | type: 'object', 84 | properties: { 85 | foo: { 86 | type: 'string', 87 | }, 88 | }, 89 | additionalProperties: true, 90 | }; 91 | 92 | const result = await convert(schema); 93 | 94 | const expected = { 95 | type: 'object', 96 | properties: { 97 | foo: { 98 | type: 'string', 99 | }, 100 | }, 101 | additionalProperties: true, 102 | }; 103 | 104 | expect(result).toEqual(expected); 105 | }); 106 | 107 | it('addionalProperties is an object', async ({ expect }) => { 108 | const schema = { 109 | $schema: 'http://json-schema.org/draft-04/schema#', 110 | type: 'object', 111 | properties: { 112 | foo: { 113 | type: 'string', 114 | }, 115 | }, 116 | additionalProperties: { 117 | type: 'object', 118 | properties: { 119 | foo: { 120 | type: 'string', 121 | format: 'date-time', 122 | }, 123 | }, 124 | }, 125 | }; 126 | 127 | const result = await convert(schema); 128 | 129 | const expected = { 130 | type: 'object', 131 | properties: { 132 | foo: { 133 | type: 'string', 134 | }, 135 | }, 136 | additionalProperties: { 137 | type: 'object', 138 | properties: { 139 | foo: { 140 | type: 'string', 141 | format: 'date-time', 142 | }, 143 | }, 144 | }, 145 | }; 146 | 147 | expect(result).toEqual(expected); 148 | }); 149 | -------------------------------------------------------------------------------- /test/readonly_writeonly.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('maintain readOnly and writeOnly props', async ({ expect }) => { 4 | const schema = { 5 | type: 'object', 6 | properties: { 7 | prop1: { 8 | type: 'string', 9 | readOnly: true, 10 | }, 11 | prop2: { 12 | type: 'string', 13 | writeOnly: true, 14 | }, 15 | }, 16 | }; 17 | 18 | const result = await convert(schema); 19 | 20 | const expected = { 21 | type: 'object', 22 | properties: { 23 | prop1: { 24 | type: 'string', 25 | readOnly: true, 26 | }, 27 | prop2: { 28 | type: 'string', 29 | writeOnly: true, 30 | }, 31 | }, 32 | }; 33 | 34 | expect(result).toEqual(expected); 35 | }); 36 | 37 | it('deep schema', async ({ expect }) => { 38 | const schema = { 39 | type: 'object', 40 | required: ['prop1', 'prop2'], 41 | properties: { 42 | prop1: { 43 | type: 'string', 44 | readOnly: true, 45 | }, 46 | prop2: { 47 | allOf: [ 48 | { 49 | type: 'object', 50 | required: ['prop3'], 51 | properties: { 52 | prop3: { 53 | type: 'object', 54 | readOnly: true, 55 | }, 56 | }, 57 | }, 58 | { 59 | type: 'object', 60 | properties: { 61 | prop4: { 62 | type: 'object', 63 | readOnly: true, 64 | }, 65 | }, 66 | }, 67 | ], 68 | }, 69 | }, 70 | }; 71 | 72 | const result = await convert(schema); 73 | 74 | const expected = { 75 | type: 'object', 76 | required: ['prop1', 'prop2'], 77 | properties: { 78 | prop1: { 79 | type: 'string', 80 | readOnly: true, 81 | }, 82 | prop2: { 83 | allOf: [ 84 | { 85 | type: 'object', 86 | required: ['prop3'], 87 | properties: { 88 | prop3: { 89 | type: 'object', 90 | readOnly: true, 91 | }, 92 | }, 93 | }, 94 | { 95 | type: 'object', 96 | properties: { 97 | prop4: { 98 | type: 'object', 99 | readOnly: true, 100 | }, 101 | }, 102 | }, 103 | ], 104 | }, 105 | }, 106 | }; 107 | 108 | expect(result).toEqual(expected); 109 | }); 110 | -------------------------------------------------------------------------------- /test/rewrite_as_extensions.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('renames illegal (unknown) keywords as extensions and skips those that already are', async ({ 4 | expect, 5 | }) => { 6 | const schema = { 7 | $schema: 'http://json-schema.org/draft-04/schema#', 8 | type: 'object', 9 | properties: { 10 | subject: { 11 | type: 'string', 12 | customProperty: true, 13 | 'x-alreadyAnExtension': true, 14 | }, 15 | }, 16 | }; 17 | 18 | const result = await convert(schema); 19 | 20 | const expected = { 21 | type: 'object', 22 | properties: { 23 | subject: { 24 | type: 'string', 25 | 'x-customProperty': true, 26 | 'x-alreadyAnExtension': true, 27 | }, 28 | }, 29 | }; 30 | 31 | expect(result).toEqual(expected); 32 | }); 33 | -------------------------------------------------------------------------------- /test/schemas/address/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "description": "An Address following the convention of http://microformats.org/wiki/hcard", 4 | "type": "object", 5 | "properties": { 6 | "post-office-box": { "type": "string" }, 7 | "extended-address": { "type": "string" }, 8 | "street-address": { "type": "string" }, 9 | "locality": { "type": "string" }, 10 | "region": { "type": "string" }, 11 | "postal-code": { "type": "string" }, 12 | "country-name": { "type": "string" } 13 | }, 14 | "required": ["locality", "region", "country-name"], 15 | "dependencies": { 16 | "post-office-box": ["street-address"], 17 | "extended-address": ["street-address"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/schemas/address/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "An Address following the convention of http://microformats.org/wiki/hcard", 3 | "type": "object", 4 | "properties": { 5 | "post-office-box": { "type": "string" }, 6 | "extended-address": { "type": "string" }, 7 | "street-address": { "type": "string" }, 8 | "locality": { "type": "string" }, 9 | "region": { "type": "string" }, 10 | "postal-code": { "type": "string" }, 11 | "country-name": { "type": "string" } 12 | }, 13 | "required": ["locality", "region", "country-name"], 14 | "allOf": [ 15 | { 16 | "oneOf": [ 17 | { "not": { "required": ["post-office-box"] } }, 18 | { "required": ["post-office-box", "street-address"] } 19 | ] 20 | }, 21 | { 22 | "oneOf": [ 23 | { "not": { "required": ["extended-address"] } }, 24 | { "required": ["extended-address", "street-address"] } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/schemas/basic/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "allOf": [ 4 | { 5 | "anyOf": [ 6 | { 7 | "type": "object", 8 | "properties": { 9 | "cats": { 10 | "type": "array", 11 | "items": { 12 | "type": "integer", 13 | "format": "int64", 14 | "example": [1] 15 | } 16 | } 17 | } 18 | }, 19 | { 20 | "type": "object", 21 | "properties": { 22 | "dogs": { 23 | "type": "array", 24 | "items": { 25 | "type": "integer", 26 | "format": "int64", 27 | "example": [1] 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | "type": "object", 34 | "properties": { 35 | "bring_cats": { 36 | "type": "array", 37 | "items": { 38 | "allOf": [ 39 | { 40 | "type": "object", 41 | "properties": { 42 | "email": { 43 | "type": "string", 44 | "example": "cats@email.com" 45 | }, 46 | "sms": { 47 | "type": ["string", "null"], 48 | "example": "+12345678" 49 | }, 50 | "properties": { 51 | "type": "object", 52 | "additionalProperties": { 53 | "type": "string" 54 | }, 55 | "example": { 56 | "name": "Wookie" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "required": ["email"] 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | } 69 | ] 70 | }, 71 | { 72 | "type": "object", 73 | "properties": { 74 | "playground": { 75 | "type": "object", 76 | "required": ["feeling", "child"], 77 | "properties": { 78 | "feeling": { 79 | "type": "string", 80 | "example": "Good feeling" 81 | }, 82 | "child": { 83 | "type": "object", 84 | "required": ["name", "age"], 85 | "properties": { 86 | "name": { 87 | "type": "string", 88 | "example": "Steven" 89 | }, 90 | "age": { 91 | "type": "integer", 92 | "example": 5 93 | } 94 | } 95 | }, 96 | "toy": { 97 | "type": "object", 98 | "properties": { 99 | "breaks_easily": { 100 | "type": "boolean", 101 | "default": false 102 | }, 103 | "color": { 104 | "type": "string", 105 | "description": "Color of the toy" 106 | }, 107 | "type": { 108 | "type": "string", 109 | "enum": ["bucket", "shovel"], 110 | "description": "Toy type" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /test/schemas/basic/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "allOf": [ 3 | { 4 | "anyOf": [ 5 | { 6 | "type": "object", 7 | "properties": { 8 | "cats": { 9 | "type": "array", 10 | "items": { 11 | "type": "integer", 12 | "format": "int64", 13 | "example": [1] 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "type": "object", 20 | "properties": { 21 | "dogs": { 22 | "type": "array", 23 | "items": { 24 | "type": "integer", 25 | "format": "int64", 26 | "example": [1] 27 | } 28 | } 29 | } 30 | }, 31 | { 32 | "type": "object", 33 | "properties": { 34 | "bring_cats": { 35 | "type": "array", 36 | "items": { 37 | "allOf": [ 38 | { 39 | "type": "object", 40 | "properties": { 41 | "email": { 42 | "type": "string", 43 | "example": "cats@email.com" 44 | }, 45 | "sms": { 46 | "type": "string", 47 | "nullable": true, 48 | "example": "+12345678" 49 | }, 50 | "properties": { 51 | "type": "object", 52 | "additionalProperties": { 53 | "type": "string" 54 | }, 55 | "example": { 56 | "name": "Wookie" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "required": ["email"] 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | } 69 | ] 70 | }, 71 | { 72 | "type": "object", 73 | "properties": { 74 | "playground": { 75 | "type": "object", 76 | "required": ["feeling", "child"], 77 | "properties": { 78 | "feeling": { 79 | "type": "string", 80 | "example": "Good feeling" 81 | }, 82 | "child": { 83 | "type": "object", 84 | "required": ["name", "age"], 85 | "properties": { 86 | "name": { 87 | "type": "string", 88 | "example": "Steven" 89 | }, 90 | "age": { 91 | "type": "integer", 92 | "example": 5 93 | } 94 | } 95 | }, 96 | "toy": { 97 | "type": "object", 98 | "properties": { 99 | "breaks_easily": { 100 | "type": "boolean", 101 | "default": false 102 | }, 103 | "color": { 104 | "type": "string", 105 | "description": "Color of the toy" 106 | }, 107 | "type": { 108 | "type": "string", 109 | "enum": ["bucket", "shovel"], 110 | "description": "Toy type" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /test/schemas/calendar/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "description": "A representation of an event", 4 | "type": "object", 5 | "required": ["dtstart", "summary"], 6 | "properties": { 7 | "dtstart": { 8 | "format": "date-time", 9 | "type": "string", 10 | "description": "Event starting time" 11 | }, 12 | "dtend": { 13 | "format": "date-time", 14 | "type": "string", 15 | "description": "Event ending time" 16 | }, 17 | "summary": { "type": "string" }, 18 | "location": { "type": "string" }, 19 | "url": { "type": "string", "format": "uri" }, 20 | "duration": { 21 | "format": "time", 22 | "type": "string", 23 | "description": "Event duration" 24 | }, 25 | "rdate": { 26 | "format": "date-time", 27 | "type": "string", 28 | "description": "Recurrence date" 29 | }, 30 | "rrule": { 31 | "type": "string", 32 | "description": "Recurrence rule" 33 | }, 34 | "category": { "type": "string" }, 35 | "description": { "type": "string" }, 36 | "geo": { "$ref": "http://json-schema.org/geo" } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/schemas/calendar/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A representation of an event", 3 | "type": "object", 4 | "required": ["dtstart", "summary"], 5 | "properties": { 6 | "dtstart": { 7 | "format": "date-time", 8 | "type": "string", 9 | "description": "Event starting time" 10 | }, 11 | "dtend": { 12 | "format": "date-time", 13 | "type": "string", 14 | "description": "Event ending time" 15 | }, 16 | "summary": { "type": "string" }, 17 | "location": { "type": "string" }, 18 | "url": { "type": "string", "format": "uri" }, 19 | "duration": { 20 | "format": "time", 21 | "type": "string", 22 | "description": "Event duration" 23 | }, 24 | "rdate": { 25 | "format": "date-time", 26 | "type": "string", 27 | "description": "Recurrence date" 28 | }, 29 | "rrule": { 30 | "type": "string", 31 | "description": "Recurrence rule" 32 | }, 33 | "category": { "type": "string" }, 34 | "description": { "type": "string" }, 35 | "geo": { "$ref": "http://json-schema.org/geo" } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/schemas/circular/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "thing": { 4 | "$ref": "#/definitions/thing" 5 | }, 6 | "person": { 7 | "properties": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "spouse": { 12 | "type": { 13 | "$ref": "#/definitions/person" 14 | } 15 | } 16 | } 17 | }, 18 | "parent": { 19 | "properties": { 20 | "name": { 21 | "type": "string" 22 | }, 23 | "children": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "#/definitions/child" 27 | } 28 | } 29 | } 30 | }, 31 | "child": { 32 | "properties": { 33 | "name": { 34 | "type": "string" 35 | }, 36 | "parents": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/parent" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/schemas/circular/openapi-circular.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "thing": { 4 | "$ref": "#/definitions/thing" 5 | }, 6 | "person": { 7 | "properties": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "spouse": { 12 | "type": { 13 | "$ref": "#/definitions/person" 14 | } 15 | } 16 | } 17 | }, 18 | "parent": { 19 | "properties": { 20 | "name": { 21 | "type": "string" 22 | }, 23 | "children": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "#/definitions/child" 27 | } 28 | } 29 | } 30 | }, 31 | "child": { 32 | "properties": { 33 | "name": { 34 | "type": "string" 35 | }, 36 | "parents": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/parent" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/schemas/circular/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "thing": { 4 | "$ref": "#/definitions/thing" 5 | }, 6 | "person": { 7 | "properties": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "spouse": { 12 | "type": { 13 | "$ref": "#/definitions/person" 14 | } 15 | } 16 | } 17 | }, 18 | "parent": { 19 | "properties": { 20 | "name": { 21 | "type": "string" 22 | }, 23 | "children": { 24 | "type": "array", 25 | "items": { 26 | "$ref": "#/definitions/child" 27 | } 28 | } 29 | } 30 | }, 31 | "child": { 32 | "properties": { 33 | "name": { 34 | "type": "string" 35 | }, 36 | "parents": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/parent" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/schemas/events/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://some.site.somewhere/event-schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Descriminate object by objectType", 5 | "type": "object", 6 | "properties": { 7 | "event": { 8 | "type": "object", 9 | "oneOf": [ 10 | { "$ref": "#/definitions/EventA" }, 11 | { "$ref": "#/definitions/EventB" } 12 | ], 13 | "required": ["objectType"], 14 | "discriminator": { 15 | "propertyName": "objectType", 16 | "mapping": { 17 | "ev-a": "#/definitions/schemas/EventA", 18 | "ev-b": "#/definitions/schemas/EventB" 19 | } 20 | } 21 | }, 22 | "health": { 23 | "type": "object", 24 | "properties": { 25 | "unavailable": { 26 | "type": "boolean", 27 | "produced-by": "health-checker" 28 | } 29 | } 30 | } 31 | }, 32 | "definitions": { 33 | "EventA": { 34 | "type": "object", 35 | "properties": { 36 | "objectType": { 37 | "type": "string" 38 | }, 39 | "infoA": { 40 | "type": "string" 41 | } 42 | } 43 | }, 44 | "EventB": { 45 | "type": "object", 46 | "properties": { 47 | "objectType": { 48 | "type": "string" 49 | }, 50 | "infoB": { 51 | "type": "number" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/schemas/events/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Descriminate object by objectType", 3 | "type": "object", 4 | "properties": { 5 | "event": { 6 | "type": "object", 7 | "oneOf": [ 8 | { 9 | "$ref": "#/definitions/EventA" 10 | }, 11 | { 12 | "$ref": "#/definitions/EventB" 13 | } 14 | ], 15 | "required": ["objectType"], 16 | "discriminator": { 17 | "propertyName": "objectType", 18 | "mapping": { 19 | "ev-a": "#/definitions/schemas/EventA", 20 | "ev-b": "#/definitions/schemas/EventB" 21 | } 22 | } 23 | }, 24 | "health": { 25 | "type": "object", 26 | "properties": { 27 | "unavailable": { 28 | "type": "boolean", 29 | "x-produced-by": "health-checker" 30 | } 31 | } 32 | } 33 | }, 34 | "definitions": { 35 | "EventA": { 36 | "type": "object", 37 | "properties": { 38 | "objectType": { 39 | "type": "string" 40 | }, 41 | "infoA": { 42 | "type": "string" 43 | } 44 | } 45 | }, 46 | "EventB": { 47 | "type": "object", 48 | "properties": { 49 | "objectType": { 50 | "type": "string" 51 | }, 52 | "infoB": { 53 | "type": "number" 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/schemas/example2/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://some.site.somewhere/entry-schema#", 3 | "$schema": "http://json-schema.org/draft-06/schema#", 4 | "description": "schema for an fstab entry", 5 | "type": "object", 6 | "required": ["storage"], 7 | "properties": { 8 | "storage": { 9 | "type": "object", 10 | "oneOf": [ 11 | { "$ref": "#/definitions/diskDevice" }, 12 | { "$ref": "#/definitions/diskUUID" }, 13 | { "$ref": "#/definitions/nfs" }, 14 | { "$ref": "#/definitions/tmpfs" } 15 | ] 16 | }, 17 | "fstype": { 18 | "enum": ["ext3", "ext4", "btrfs"] 19 | }, 20 | "options": { 21 | "type": "array", 22 | "minItems": 1, 23 | "items": { "type": "string" }, 24 | "uniqueItems": true 25 | }, 26 | "readonly": { "type": "boolean" } 27 | }, 28 | "definitions": { 29 | "diskDevice": { 30 | "properties": { 31 | "type": { "enum": ["disk"] }, 32 | "device": { 33 | "type": "string", 34 | "pattern": "^/dev/[^/]+(/[^/]+)*$" 35 | } 36 | }, 37 | "required": ["type", "device"], 38 | "additionalProperties": false 39 | }, 40 | "diskUUID": { 41 | "properties": { 42 | "type": { "enum": ["disk"] }, 43 | "label": { 44 | "type": "string", 45 | "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" 46 | } 47 | }, 48 | "required": ["type", "label"], 49 | "additionalProperties": false 50 | }, 51 | "nfs": { 52 | "properties": { 53 | "type": { "enum": ["nfs"] }, 54 | "remotePath": { 55 | "type": "string", 56 | "pattern": "^(/[^/]+)+$" 57 | }, 58 | "server": { 59 | "type": "string", 60 | "oneOf": [ 61 | { "format": "hostname" }, 62 | { "format": "ipv4" }, 63 | { "format": "ipv6" } 64 | ] 65 | } 66 | }, 67 | "required": ["type", "server", "remotePath"], 68 | "additionalProperties": false 69 | }, 70 | "tmpfs": { 71 | "properties": { 72 | "type": { "enum": ["tmpfs"] }, 73 | "sizeInMB": { 74 | "type": "integer", 75 | "minimum": 16, 76 | "maximum": 512 77 | } 78 | }, 79 | "required": ["type", "sizeInMB"], 80 | "additionalProperties": false 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/schemas/example2/openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "schema for an fstab entry", 3 | "type": "object", 4 | "required": ["storage"], 5 | "properties": { 6 | "storage": { 7 | "type": "object", 8 | "oneOf": [ 9 | { "$ref": "#/definitions/diskDevice" }, 10 | { "$ref": "#/definitions/diskUUID" }, 11 | { "$ref": "#/definitions/nfs" }, 12 | { "$ref": "#/definitions/tmpfs" } 13 | ] 14 | }, 15 | "fstype": { 16 | "enum": ["ext3", "ext4", "btrfs"] 17 | }, 18 | "options": { 19 | "type": "array", 20 | "minItems": 1, 21 | "items": { "type": "string" }, 22 | "uniqueItems": true 23 | }, 24 | "readonly": { "type": "boolean" } 25 | }, 26 | "definitions": { 27 | "diskDevice": { 28 | "properties": { 29 | "type": { "enum": ["disk"] }, 30 | "device": { 31 | "type": "string", 32 | "pattern": "^/dev/[^/]+(/[^/]+)*$" 33 | } 34 | }, 35 | "required": ["type", "device"], 36 | "additionalProperties": false 37 | }, 38 | "diskUUID": { 39 | "properties": { 40 | "type": { "enum": ["disk"] }, 41 | "label": { 42 | "type": "string", 43 | "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" 44 | } 45 | }, 46 | "required": ["type", "label"], 47 | "additionalProperties": false 48 | }, 49 | "nfs": { 50 | "properties": { 51 | "type": { "enum": ["nfs"] }, 52 | "remotePath": { 53 | "type": "string", 54 | "pattern": "^(/[^/]+)+$" 55 | }, 56 | "server": { 57 | "type": "string", 58 | "oneOf": [ 59 | { "format": "hostname" }, 60 | { "format": "ipv4" }, 61 | { "format": "ipv6" } 62 | ] 63 | } 64 | }, 65 | "required": ["type", "server", "remotePath"], 66 | "additionalProperties": false 67 | }, 68 | "tmpfs": { 69 | "properties": { 70 | "type": { "enum": ["tmpfs"] }, 71 | "sizeInMB": { 72 | "type": "integer", 73 | "minimum": 16, 74 | "maximum": 512 75 | } 76 | }, 77 | "required": ["type", "sizeInMB"], 78 | "additionalProperties": false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/schemas/invalid/json-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "allOf": [ 3 | { 4 | "anyOf": [ 5 | { 6 | "type": "object", 7 | "properties": { 8 | "cats": { 9 | "type": "array", 10 | "items": { 11 | "type": "integer", 12 | "format": "int64", 13 | "example": [1] 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "type": "object", 20 | "properties": { 21 | "dogs": { 22 | "type": "array", 23 | "items": { 24 | "type": "integer", 25 | "format": "int64", 26 | "example": [1] 27 | } 28 | } 29 | } 30 | }, 31 | { 32 | "type": "object", 33 | "properties": { 34 | "bring_cats": { 35 | "type": "array", 36 | "items": { 37 | "allOf": [ 38 | { 39 | "type": "object", 40 | "properties": { 41 | "email": { 42 | "type": "string", 43 | "example": "cats@email.com" 44 | }, 45 | "sms": { 46 | "type": "string", 47 | "nullable": true, 48 | "example": "+12345678" 49 | }, 50 | "properties": { 51 | "type": "object", 52 | "additionalProperties": { 53 | "type": "invalidtype" 54 | }, 55 | "example": { 56 | "name": "Wookie" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "required": ["email"] 63 | } 64 | ] 65 | } 66 | } 67 | } 68 | } 69 | ] 70 | }, 71 | { 72 | "type": "object", 73 | "properties": { 74 | "playground": { 75 | "type": "object", 76 | "required": ["feeling", "child"], 77 | "properties": { 78 | "feeling": { 79 | "type": "string", 80 | "example": "Good feeling" 81 | }, 82 | "child": { 83 | "type": "object", 84 | "required": ["name", "age"], 85 | "properties": { 86 | "name": { 87 | "type": "string", 88 | "example": "Steven" 89 | }, 90 | "age": { 91 | "type": "integer", 92 | "example": 5 93 | } 94 | } 95 | }, 96 | "toy": { 97 | "type": "object", 98 | "properties": { 99 | "breaks_easily": { 100 | "type": "boolean", 101 | "default": false 102 | }, 103 | "color": { 104 | "type": "string", 105 | "description": "Color of the toy" 106 | }, 107 | "type": { 108 | "type": "string", 109 | "enum": ["bucket", "shovel"], 110 | "description": "Toy type" 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /test/subschema.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('strips $id from all subschemas not just root`', async ({ expect }) => { 4 | const schema = { 5 | $id: 'https://foo/bla', 6 | id: 'https://foo/bla', 7 | $schema: 'http://json-schema.org/draft-06/schema#', 8 | type: 'object', 9 | properties: { 10 | foo: { 11 | $id: '/properties/foo', 12 | type: 'array', 13 | items: { 14 | $id: '/properties/foo/items', 15 | type: 'object', 16 | properties: { 17 | id: { 18 | $id: '/properties/foo/items/properties/id', 19 | type: 'string', 20 | }, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | const result = await convert(schema); 28 | 29 | const expected = { 30 | type: 'object', 31 | properties: { 32 | foo: { 33 | type: 'array', 34 | items: { 35 | type: 'object', 36 | properties: { 37 | id: { 38 | type: 'string', 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }; 45 | expect(result).toEqual(expected); 46 | }); 47 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ESNext", "dom"], 6 | "types": ["vitest/globals"], 7 | "rootDir": "../" 8 | }, 9 | "include": ["."] 10 | } 11 | -------------------------------------------------------------------------------- /test/type-array-split.test.ts: -------------------------------------------------------------------------------- 1 | import convert from '../src'; 2 | 3 | it('splits type arrays correctly', async ({ expect }) => { 4 | const schema = { 5 | $schema: 'http://json-schema.org/draft-04/schema#', 6 | type: 'object', 7 | properties: { 8 | emptyArray: { 9 | type: [], 10 | }, 11 | arrayWithNull: { 12 | type: ['null'], 13 | }, 14 | arrayWithSingleType: { 15 | type: ['string'], 16 | }, 17 | arrayWithNullAndSingleType: { 18 | type: ['null', 'string'], 19 | }, 20 | arrayWithNullAndMultipleTypes: { 21 | type: ['null', 'string', 'number'], 22 | }, 23 | arrayWithMultipleTypes: { 24 | type: ['string', 'number'], 25 | }, 26 | }, 27 | }; 28 | 29 | const result = await convert(schema); 30 | 31 | const expected = { 32 | type: 'object', 33 | properties: { 34 | emptyArray: {}, 35 | arrayWithNull: { 36 | nullable: true, 37 | }, 38 | arrayWithSingleType: { 39 | type: 'string', 40 | }, 41 | arrayWithNullAndSingleType: { 42 | nullable: true, 43 | type: 'string', 44 | }, 45 | arrayWithNullAndMultipleTypes: { 46 | nullable: true, 47 | anyOf: [{ type: 'string' }, { type: 'number' }], 48 | }, 49 | arrayWithMultipleTypes: { 50 | anyOf: [{ type: 'string' }, { type: 'number' }], 51 | }, 52 | }, 53 | }; 54 | 55 | expect(result).toEqual(expected); 56 | }); 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2020", 5 | "allowJs": true, 6 | "allowSyntheticDefaultImports": true, 7 | "baseUrl": "src", 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "inlineSourceMap": false, 11 | "lib": ["esnext"], 12 | "listEmittedFiles": false, 13 | "listFiles": false, 14 | "moduleResolution": "Bundler", 15 | "noFallthroughCasesInSwitch": true, 16 | "pretty": true, 17 | "resolveJsonModule": true, 18 | "rootDir": "src", 19 | "skipLibCheck": true, 20 | "noImplicitAny": false, 21 | "strict": true, 22 | "noUnusedParameters": true, 23 | "sourceMap": true 24 | }, 25 | "compileOnSave": false, 26 | "exclude": ["node_modules", "dist", "coverage", "bin"], 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | watch: false, 7 | isolate: false, 8 | reporters: 'verbose', 9 | }, 10 | esbuild: { 11 | target: 'node22', 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------