├── .dependency-cruiser.js ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── actions │ └── install-node-modules │ │ └── action.yml ├── release-drafter.yml └── workflows │ ├── draft-or-update-next-release.yml │ ├── release.yml │ ├── sync-readme-sponsors.yml │ └── test-pr.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets ├── header-large.jpg ├── header-round-medium.png ├── logo.ai └── plus-sign.png ├── babel.config.js ├── builds └── deno │ ├── index.d.ts │ ├── index.js │ └── index.mjs ├── documentation └── FAQs │ ├── applying-from-schema-on-generics.md │ ├── does-json-schema-to-ts-work-on-json-file-schemas.md │ ├── i-get-a-type-instantiation-is-excessively-deep-and-potentially-infinite-error-what-should-i-do.md │ ├── type-instantiation-is-excessively-deep-and-possibly-infinite.png │ └── will-json-schema-to-ts-impact-the-performances-of-my-ide-compiler.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── scripts └── setPackageVersion.ts ├── src ├── definitions │ ├── deserializationPattern.ts │ ├── extendedJsonSchema.ts │ ├── fromSchemaOptions.ts │ ├── index.ts │ ├── jsonSchema.ts │ └── jsonSchema.type.test.ts ├── index.ts ├── parse-options.ts ├── parse-options.type.test.ts ├── parse-schema │ ├── ajv.util.test.ts │ ├── allOf.ts │ ├── allOf.unit.test.ts │ ├── anyOf.ts │ ├── anyOf.unit.test.ts │ ├── array.ts │ ├── array.unit.test.ts │ ├── const.ts │ ├── const.unit.test.ts │ ├── deserialize.ts │ ├── deserialize.type.test.ts │ ├── enum.ts │ ├── enum.unit.test.ts │ ├── ifThenElse.ts │ ├── ifThenElse.unit.test.ts │ ├── index.ts │ ├── multipleTypes.ts │ ├── multipleTypes.unit.test.ts │ ├── noSchema.unit.test.ts │ ├── not.ts │ ├── not.unit.test.ts │ ├── nullable.ts │ ├── nullable.unit.test.ts │ ├── object.ts │ ├── object.unit.test.ts │ ├── oneOf.ts │ ├── oneOf.unit.test.ts │ ├── references │ │ ├── external.ts │ │ ├── external.unit.test.ts │ │ ├── index.ts │ │ ├── internal.ts │ │ ├── internal.unit.test.ts │ │ └── utils.ts │ ├── singleType.boolean.unit.test.ts │ ├── singleType.integer.unit.test.ts │ ├── singleType.null.unit.test.ts │ ├── singleType.number.unit.test.ts │ ├── singleType.string.unit.test.ts │ ├── singleType.ts │ └── utils.ts ├── tests │ └── readme │ │ ├── allOf.type.test.ts │ │ ├── anyOf.type.test.ts │ │ ├── array.type.test.ts │ │ ├── const.type.test.ts │ │ ├── definitions.test.ts │ │ ├── deserialization.test.ts │ │ ├── enum.type.test.ts │ │ ├── extensions.type.test.ts │ │ ├── ifThenElse.type.test.ts │ │ ├── intro.type.test.ts │ │ ├── not.type.test.ts │ │ ├── nullable.type.test.ts │ │ ├── object.type.test.ts │ │ ├── oneOf.type.test.ts │ │ ├── primitive.type.test.ts │ │ ├── references.test.ts │ │ └── tuple.type.test.ts ├── type-utils │ ├── and.ts │ ├── extends.ts │ ├── get.ts │ ├── if.ts │ ├── index.ts │ ├── join.ts │ ├── key.ts │ ├── narrow.ts │ ├── not.ts │ ├── pop.ts │ ├── split.ts │ ├── tail.ts │ └── writable.ts └── utils │ ├── asConst.ts │ ├── asConst.type.test.ts │ ├── asConst.unit.test.ts │ ├── index.ts │ └── type-guards │ ├── ajv.util.test.ts │ ├── compiler.ts │ ├── compiler.unit.test.ts │ ├── index.ts │ ├── schema.util.test.ts │ ├── validator.ts │ └── validator.unit.test.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.dependency-cruiser.js: -------------------------------------------------------------------------------- 1 | /** @type {import('dependency-cruiser').IConfiguration} */ 2 | module.exports = { 3 | forbidden: [ 4 | { 5 | name: "no-circular", 6 | severity: "error", 7 | comment: 8 | "This dependency is part of a circular relationship. You might want to revise " + 9 | "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", 10 | from: { path: ["src"] }, 11 | to: { 12 | circular: true, 13 | }, 14 | }, 15 | ], 16 | options: { 17 | doNotFollow: { 18 | path: "node_modules", 19 | dependencyTypes: [ 20 | "npm", 21 | "npm-dev", 22 | "npm-optional", 23 | "npm-peer", 24 | "npm-bundled", 25 | "npm-no-pkg", 26 | ], 27 | }, 28 | 29 | moduleSystems: ["amd", "cjs", "es6", "tsd"], 30 | 31 | tsPreCompilationDeps: true, 32 | 33 | tsConfig: { 34 | fileName: "tsconfig.json", 35 | }, 36 | 37 | enhancedResolveOptions: { 38 | exportsFields: ["exports"], 39 | 40 | conditionNames: ["import", "require", "node", "default"], 41 | }, 42 | reporterOptions: { 43 | dot: { 44 | collapsePattern: "node_modules/[^/]+", 45 | }, 46 | archi: { 47 | collapsePattern: 48 | "^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/[^/]+", 49 | }, 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | builds 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prefer-arrow", "import", "prettier", "unused-imports", "jsdoc"], 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:import/recommended", 6 | "plugin:prettier/recommended", 7 | "prettier", 8 | "plugin:jsdoc/recommended", 9 | ], 10 | rules: { 11 | "jsdoc/require-jsdoc": [ 12 | "error", 13 | { 14 | contexts: [ 15 | "TSTypeAliasDeclaration", 16 | "TSInterfaceDeclaration", 17 | "TSMethodSignature", 18 | // "TSPropertySignature", 19 | "TSDeclareFunction", 20 | "TSEnumDeclaration", 21 | ], 22 | require: { 23 | ArrowFunctionExpression: true, 24 | ClassDeclaration: true, 25 | ClassExpression: true, 26 | FunctionDeclaration: true, 27 | FunctionExpression: true, 28 | MethodDefinition: true, 29 | }, 30 | }, 31 | ], 32 | "jsdoc/require-param-type": "off", 33 | "jsdoc/require-returns-type": "off", 34 | "prettier/prettier": "error", 35 | "import/extensions": "off", 36 | "import/no-unresolved": ["error", { caseSensitiveStrict: true }], 37 | "import/prefer-default-export": "off", 38 | "import/no-duplicates": "error", 39 | complexity: ["error", 8], 40 | "max-lines": ["error", 200], 41 | "max-depth": ["error", 3], 42 | "max-params": ["error", 4], 43 | eqeqeq: ["error", "smart"], 44 | "import/no-extraneous-dependencies": [ 45 | "error", 46 | { 47 | devDependencies: true, 48 | optionalDependencies: false, 49 | peerDependencies: false, 50 | }, 51 | ], 52 | "no-shadow": ["error", { hoist: "all" }], 53 | "prefer-const": "error", 54 | "padding-line-between-statements": [ 55 | "error", 56 | { 57 | blankLine: "always", 58 | prev: "*", 59 | next: "return", 60 | }, 61 | ], 62 | "prefer-arrow/prefer-arrow-functions": [ 63 | "error", 64 | { 65 | disallowPrototype: true, 66 | singleReturnOnly: false, 67 | classPropertiesAllowed: false, 68 | }, 69 | ], 70 | "no-restricted-imports": [ 71 | "error", 72 | { 73 | paths: [ 74 | { 75 | name: "lodash", 76 | message: "Please use lodash/{module} import instead", 77 | }, 78 | { 79 | name: ".", 80 | message: "Please use explicit import file", 81 | }, 82 | ], 83 | }, 84 | ], 85 | curly: ["error", "all"], 86 | "arrow-body-style": ["error", "as-needed"], 87 | }, 88 | settings: { 89 | jsdoc: { 90 | ignorePrivate: true, 91 | ignoreInternal: true, 92 | }, 93 | }, 94 | root: true, 95 | env: { 96 | es6: true, 97 | node: true, 98 | jest: true, 99 | browser: true, 100 | }, 101 | parserOptions: { 102 | ecmaVersion: 9, 103 | sourceType: "module", 104 | }, 105 | overrides: [ 106 | { 107 | files: ["**/*.ts?(x)"], 108 | extends: [ 109 | "plugin:@typescript-eslint/recommended", 110 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 111 | "plugin:prettier/recommended", 112 | ], 113 | parser: "@typescript-eslint/parser", 114 | parserOptions: { 115 | project: "tsconfig.json", 116 | }, 117 | settings: { "import/resolver": { typescript: {} } }, 118 | rules: { 119 | "@typescript-eslint/prefer-optional-chain": "error", 120 | "no-shadow": "off", 121 | "@typescript-eslint/no-shadow": "error", 122 | "@typescript-eslint/prefer-nullish-coalescing": "error", 123 | "@typescript-eslint/strict-boolean-expressions": "error", 124 | "@typescript-eslint/ban-ts-comment": "off", 125 | "@typescript-eslint/explicit-function-return-type": "off", 126 | "@typescript-eslint/explicit-member-accessibility": "off", 127 | "@typescript-eslint/camelcase": "off", 128 | "unused-imports/no-unused-imports": "error", 129 | "@typescript-eslint/interface-name-prefix": "off", 130 | "@typescript-eslint/explicit-module-boundary-types": "error", 131 | "@typescript-eslint/no-explicit-any": "error", 132 | "@typescript-eslint/no-unused-vars": "error", 133 | "@typescript-eslint/ban-types": "off", 134 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", 135 | "@typescript-eslint/no-unnecessary-condition": "error", 136 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 137 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 138 | "@typescript-eslint/switch-exhaustiveness-check": "error", 139 | // plugin:prettier/recommended turns off arrow-body-style so it is turned back on here 140 | // But a bug can occur and prettier can provide an invalid code (missing closing parenthesis) 141 | // More details here: https://github.com/prettier/eslint-plugin-prettier#arrow-body-style-and-prefer-arrow-callback-issue 142 | "arrow-body-style": ["error", "as-needed"], 143 | }, 144 | }, 145 | { 146 | files: ["**/*.test.ts", "scripts/*.ts"], 147 | rules: { 148 | "max-lines": ["off"], 149 | "jsdoc/require-jsdoc": ["off"], 150 | }, 151 | }, 152 | ], 153 | }; 154 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ThomasAribart] 4 | -------------------------------------------------------------------------------- /.github/actions/install-node-modules/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Node Dependencies 2 | description: Install dependencies using yarn 3 | inputs: 4 | node-version: 5 | description: Node version to use 6 | required: true 7 | typescript-version: 8 | description: TS version to use 9 | default: "default" 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Use Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ inputs.node-version }} 18 | 19 | - name: Get yarn cache directory path 20 | id: yarn-cache-dir-path 21 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 22 | shell: bash 23 | 24 | - name: Sync yarn cache 25 | uses: actions/cache@v3 26 | with: 27 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | - name: Sync node_modules cache 33 | id: sync-node-modules-cache 34 | uses: actions/cache@v3 35 | with: 36 | path: "**/node_modules" 37 | key: ${{ runner.os }}-modules-${{ inputs.node-version }}-${{ inputs.typescript-version }}-${{ hashFiles('./yarn.lock') }} 38 | 39 | - name: Install node_modules 40 | run: if [ '${{ steps.sync-node-modules-cache.outputs.cache-hit }}' != 'true' ]; then yarn install --immutable; fi 41 | shell: bash 42 | 43 | - name: Override TS with specified version 44 | run: if [ '${{ inputs.typescript-version }}' != 'default' ] && [ '${{ steps.sync-node-modules-cache.outputs.cache-hit }}' != 'true' ]; then yarn add --dev typescript@${{ inputs.typescript-version }}; fi 45 | shell: bash 46 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION 🌈" 2 | tag-template: "v$RESOLVED_VERSION" 3 | version-resolver: 4 | major: 5 | labels: 6 | - major 7 | minor: 8 | labels: 9 | - minor 10 | patch: 11 | labels: 12 | - patch 13 | default: patch 14 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 15 | change-title-escapes: '\<*_&' 16 | template: | 17 | ## Changes 18 | 19 | $CHANGES 20 | -------------------------------------------------------------------------------- /.github/workflows/draft-or-update-next-release.yml: -------------------------------------------------------------------------------- 1 | name: 📝 Draft or update next release 2 | concurrency: draft_or_update_next_release 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | draft-or-update-next-release: 11 | name: 📝 Draft/update next release 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | steps: 15 | - name: ♻️ Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: 📝 Draft/update next release 19 | uses: release-drafter/release-drafter@v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Release to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | CI: true 9 | 10 | jobs: 11 | build-and-test: 12 | name: 🎯 Run tests (Node ${{ matrix.node }} / TS ${{ matrix.typescript }}) 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node: [16, 18, 20] 17 | typescript: 18 | [ 19 | "~4.6.4", 20 | "~4.7.4", 21 | "~4.8.3", 22 | "~4.9.5", 23 | "~5.0.4", 24 | "~5.1.6", 25 | "~5.2.2", 26 | "~5.3.3", 27 | "~5.4.2", 28 | "latest", 29 | ] 30 | steps: 31 | - name: ♻️ Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: 🚚 Install node_modules 35 | uses: ./.github/actions/install-node-modules 36 | with: 37 | node-version: ${{ matrix.node }} 38 | typescript-version: ${{ matrix.typescript }} 39 | 40 | - name: 🎯 Run tests 41 | run: yarn test 42 | 43 | release: 44 | name: 🚀 Release 45 | runs-on: ubuntu-latest 46 | needs: build-and-test 47 | steps: 48 | - name: ♻️ Checkout 49 | uses: actions/checkout@v3 50 | with: 51 | ref: main 52 | 53 | - name: 🚚 Install node_modules 54 | uses: ./.github/actions/install-node-modules 55 | with: 56 | node-version: 18 57 | 58 | - name: 🗑️ Clear lib directory 59 | run: rm -rf lib 60 | 61 | - name: 🗑️ Clear builds directory 62 | run: rm -rf builds 63 | 64 | - name: 📌 Set package version 65 | run: yarn set-package-version ${{ github.event.release.tag_name }} 66 | 67 | - name: 🏗️ Build 68 | run: yarn build 69 | 70 | - name: 🏗️ Build for Deno 71 | run: yarn rollup -c 72 | 73 | - name: 💾 Commit Deno Build 74 | uses: EndBug/add-and-commit@v9.1.3 75 | with: 76 | default_author: github_actions 77 | message: "${{ github.event.release.tag_name }} release" 78 | 79 | - name: 🚀 Release 80 | uses: JS-DevTools/npm-publish@v2 81 | with: 82 | token: ${{ secrets.NPM_TOKEN }} 83 | strategy: upgrade 84 | -------------------------------------------------------------------------------- /.github/workflows/sync-readme-sponsors.yml: -------------------------------------------------------------------------------- 1 | name: 💖 Sync README sponsors 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: 00 12 1,15 * * 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | sync-readme-sponsors: 13 | name: 💖 Sync README sponsors 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: ♻️ Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | ref: main 20 | 21 | - name: 💖 Sync README sponsors 22 | uses: JamesIves/github-sponsors-readme-action@v1.5.0 23 | with: 24 | token: ${{ secrets.PAT }} 25 | file: README.md 26 | template: {{ name }}   27 | active-only: false 28 | 29 | - name: 💾 Commit README 30 | uses: EndBug/add-and-commit@v9.1.3 31 | with: 32 | default_author: github_actions 33 | message: "Automatically synchronize README sponsors" 34 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | name: 🎯 Test PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | env: 8 | CI: true 9 | 10 | jobs: 11 | build-and-test: 12 | name: 🎯 Run tests (Node ${{ matrix.node }} / TS ${{ matrix.typescript }}) 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node: [16, 18, 20] 17 | typescript: 18 | [ 19 | "~4.6.4", 20 | "~4.7.4", 21 | "~4.8.3", 22 | "~4.9.5", 23 | "~5.0.4", 24 | "~5.1.6", 25 | "~5.2.2", 26 | "~5.3.3", 27 | "~5.4.2", 28 | "latest", 29 | ] 30 | timeout-minutes: 30 31 | steps: 32 | - name: ♻️ Checkout 33 | uses: actions/checkout@v3 34 | with: 35 | ref: ${{ github.event.pull_request.head.sha }} 36 | fetch-depth: 0 37 | 38 | - name: 🚚 Install node_modules 39 | uses: ./.github/actions/install-node-modules 40 | with: 41 | node-version: ${{ matrix.node }} 42 | typescript-version: ${{ matrix.typescript }} 43 | 44 | - name: 🎯 Run tests 45 | run: yarn test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | assets 4 | node_modules 5 | src 6 | scripts 7 | builds 8 | .gitignore 9 | tsconfig.json 10 | tsconfig.build.json 11 | rollup.config.js 12 | babel.config.js 13 | jest.config.js 14 | yarn.lock 15 | documentation 16 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.19.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | **/coverage 3 | **/lib 4 | **/builds 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid", 5 | "importOrder": ["^~/(.*)$", "^[./]"], 6 | "importOrderSeparation": true, 7 | "importOrderSortSpecifiers": true, 8 | "importOrderCaseInsensitive": true, 9 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 10 | "importOrderParserPlugins": ["importAssertions", "typescript", "jsx"] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | "search.exclude": { 6 | "**/node_modules": true, 7 | "**/builds": true, 8 | ".yarn": true, 9 | "yarn.lock": true, 10 | "lib": true 11 | }, 12 | "files.exclude": { 13 | "**/.git": true, 14 | "**/.DS_Store": true, 15 | "**/builds": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thomas Aribart 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 | -------------------------------------------------------------------------------- /assets/header-large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAribart/json-schema-to-ts/116deb42b968da708cdea771d3034ce52f699f45/assets/header-large.jpg -------------------------------------------------------------------------------- /assets/header-round-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAribart/json-schema-to-ts/116deb42b968da708cdea771d3034ce52f699f45/assets/header-round-medium.png -------------------------------------------------------------------------------- /assets/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAribart/json-schema-to-ts/116deb42b968da708cdea771d3034ce52f699f45/assets/logo.ai -------------------------------------------------------------------------------- /assets/plus-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAribart/json-schema-to-ts/116deb42b968da708cdea771d3034ce52f699f45/assets/plus-sign.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const defaultPresets = [ 2 | [ 3 | "@babel/preset-typescript", 4 | { allowNamespaces: true, allowDeclareFields: true }, 5 | ], 6 | ]; 7 | 8 | module.exports = { 9 | env: { 10 | cjs: { 11 | presets: [["@babel/preset-env", { modules: "cjs" }], ...defaultPresets], 12 | }, 13 | esm: { 14 | presets: [["@babel/preset-env", { modules: false }], ...defaultPresets], 15 | }, 16 | }, 17 | ignore: [/.*\/(.*\.|)test\.tsx?/, /node_modules/, /lib/, /builds/], 18 | plugins: [ 19 | [ 20 | "module-resolver", 21 | { 22 | root: ["./src"], 23 | extensions: [".ts"], 24 | alias: { "~": "./src" }, 25 | }, 26 | ], 27 | "@babel/plugin-transform-runtime", 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /builds/deno/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | Object.defineProperty(exports, '__esModule', { value: true }); 6 | 7 | /****************************************************************************** 8 | Copyright (c) Microsoft Corporation. 9 | 10 | Permission to use, copy, modify, and/or distribute this software for any 11 | purpose with or without fee is hereby granted. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 15 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 18 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | PERFORMANCE OF THIS SOFTWARE. 20 | ***************************************************************************** */ 21 | 22 | function __spreadArray(to, from, pack) { 23 | if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { 24 | if (ar || !(i in from)) { 25 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 26 | ar[i] = from[i]; 27 | } 28 | } 29 | return to.concat(ar || Array.prototype.slice.call(from)); 30 | } 31 | 32 | var wrapCompilerAsTypeGuard = function (compiler) { 33 | return function (schema) { 34 | var compilingOptions = []; 35 | for (var _i = 1; _i < arguments.length; _i++) { 36 | compilingOptions[_i - 1] = arguments[_i]; 37 | } 38 | var validator = compiler.apply(void 0, __spreadArray([schema], compilingOptions, false)); 39 | return function (data) { 40 | var validationOptions = []; 41 | for (var _i = 1; _i < arguments.length; _i++) { 42 | validationOptions[_i - 1] = arguments[_i]; 43 | } 44 | return validator.apply(void 0, __spreadArray([data], validationOptions, false)); 45 | }; 46 | }; 47 | }; 48 | 49 | var wrapValidatorAsTypeGuard = function (validator) { 50 | return function (schema, data) { 51 | var validationOptions = []; 52 | for (var _i = 2; _i < arguments.length; _i++) { 53 | validationOptions[_i - 2] = arguments[_i]; 54 | } 55 | return validator.apply(void 0, __spreadArray([schema, data], validationOptions, false)); 56 | }; 57 | }; 58 | 59 | var asConst = function (input) { return input; }; 60 | 61 | exports.asConst = asConst; 62 | exports.wrapCompilerAsTypeGuard = wrapCompilerAsTypeGuard; 63 | exports.wrapValidatorAsTypeGuard = wrapValidatorAsTypeGuard; 64 | -------------------------------------------------------------------------------- /builds/deno/index.mjs: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /****************************************************************************** 4 | Copyright (c) Microsoft Corporation. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | ***************************************************************************** */ 17 | 18 | function __spreadArray(to, from, pack) { 19 | if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { 20 | if (ar || !(i in from)) { 21 | if (!ar) ar = Array.prototype.slice.call(from, 0, i); 22 | ar[i] = from[i]; 23 | } 24 | } 25 | return to.concat(ar || Array.prototype.slice.call(from)); 26 | } 27 | 28 | var wrapCompilerAsTypeGuard = function (compiler) { 29 | return function (schema) { 30 | var compilingOptions = []; 31 | for (var _i = 1; _i < arguments.length; _i++) { 32 | compilingOptions[_i - 1] = arguments[_i]; 33 | } 34 | var validator = compiler.apply(void 0, __spreadArray([schema], compilingOptions, false)); 35 | return function (data) { 36 | var validationOptions = []; 37 | for (var _i = 1; _i < arguments.length; _i++) { 38 | validationOptions[_i - 1] = arguments[_i]; 39 | } 40 | return validator.apply(void 0, __spreadArray([data], validationOptions, false)); 41 | }; 42 | }; 43 | }; 44 | 45 | var wrapValidatorAsTypeGuard = function (validator) { 46 | return function (schema, data) { 47 | var validationOptions = []; 48 | for (var _i = 2; _i < arguments.length; _i++) { 49 | validationOptions[_i - 2] = arguments[_i]; 50 | } 51 | return validator.apply(void 0, __spreadArray([schema, data], validationOptions, false)); 52 | }; 53 | }; 54 | 55 | var asConst = function (input) { return input; }; 56 | 57 | export { asConst, wrapCompilerAsTypeGuard, wrapValidatorAsTypeGuard }; 58 | -------------------------------------------------------------------------------- /documentation/FAQs/applying-from-schema-on-generics.md: -------------------------------------------------------------------------------- 1 | # How can I apply `FromSchema` on generics? 2 | 3 | Let's say that your are building a library on top of JSON Schemas, for instance a function that builds mocked data from schemas. You might want to benefit from inferred typings thanks to `json-schema-to-ts`: 4 | 5 | ```ts 6 | import type { FromSchema, JSONSchema } from "json-schema-to-ts"; 7 | 8 | type Mocker = (schema: SCHEMA) => FromSchema; 9 | 10 | const getMockedData: Mocker = schema => { 11 | ... // logic here 12 | }; 13 | 14 | const dogSchema = { 15 | type: "object", 16 | ... // schema here 17 | } as const; 18 | 19 | const dogMock = getMockedData(dogSchema); 20 | ``` 21 | 22 | Sadly, for some reasons that I don't fully grasp, TypeScript will raise an `type instantiation is excessively deep and possibly infinite` error: 23 | 24 | ![type instantiation is excessively deep and possibly infinite error](./type-instantiation-is-excessively-deep-and-possibly-infinite.png) 25 | 26 | You can still make it work by adding a second generic with the corresponding default: 27 | 28 | ```ts 29 | import { FromSchema, JSONSchema } from "json-schema-to-ts"; 30 | 31 | type Mocker = >( 32 | schema: SCHEMA, 33 | ) => DATA; 34 | 35 | const getMockedData: Mocker = schema => { 36 | ... // logic here 37 | }; 38 | 39 | const dogSchema = { 40 | type: "object", 41 | ... // schema here 42 | } as const; 43 | 44 | // 🙌 Will work! 45 | const dogMock = getMockedData(dogSchema); 46 | ``` 47 | -------------------------------------------------------------------------------- /documentation/FAQs/does-json-schema-to-ts-work-on-json-file-schemas.md: -------------------------------------------------------------------------------- 1 | # Does `json-schema-to-ts` work on _.json_ file schemas? 2 | 3 | Sadly, no 😭 4 | 5 | `FromSchema` is based on type computations. By design, it only works on "good enough" material, i.e. _narrow_ types (`{ type: "string" }`) and NOT _widened_ ones (`{ type: string }` which can also represent `{ type: "number" }`). However, JSON imports are **widened by default**. This is native TS behavior, there's no changing that. 6 | 7 | If you really want use _.json_ files, you can start by [upvoting this feature request to implement _.json_ imports `as const`](https://github.com/microsoft/TypeScript/issues/32063) on the official repo 🙂 AND you can always cast imported schemas as their narrow types: 8 | 9 | ```json 10 | // dog.json 11 | { 12 | "type": "object", 13 | "properties": { 14 | "name": { "type": "string" }, 15 | "age": { "type": "integer" }, 16 | "hobbies": { "type": "array", "items": { "type": "string" } }, 17 | "favoriteFood": { "enum": ["pizza", "taco", "fries"] } 18 | }, 19 | "required": ["name", "age"] 20 | } 21 | ``` 22 | 23 | ```typescript 24 | import { FromSchema } from "json-schema-to-ts"; 25 | 26 | import dogRawSchema from "./dog.json"; 27 | 28 | const dogSchema = dogRawSchema as { 29 | type: "object"; 30 | properties: { 31 | name: { type: "string" }; 32 | age: { type: "integer" }; 33 | hobbies: { type: "array"; items: { type: "string" } }; 34 | favoriteFood: { enum: ["pizza", "taco", "fries"] }; 35 | }; 36 | required: ["name", "age"]; 37 | }; 38 | 39 | type Dog = FromSchema; 40 | // => Will work 🙌 41 | ``` 42 | 43 | It is technically code duplication, BUT TS will throw an errow if the narrow and widened types don't sufficiently overlap, which allows for partial type safety (roughly, everything but the object "leafs"). In particular, this will work well on object properties names, as object keys are not widened by default. 44 | 45 | ```typescript 46 | import { FromSchema } from "json-schema-to-ts"; 47 | 48 | import dogRawSchema from "./dog.json"; 49 | 50 | const dogSchema = dogoRawSchema as { 51 | type: "object"; 52 | properties: { 53 | name: { type: "number" }; // "number" instead of "string" will go undetected... 54 | years: { type: "integer" }; // ...but "years" instead of "age" will not 🙌 55 | hobbies: { type: "array"; items: { type: "string" } }; 56 | favoriteFood: { const: "pizza" }; // ..."const" instead of "enum" as well 🙌 57 | }; 58 | required: ["name", "age"]; 59 | }; 60 | ``` 61 | -------------------------------------------------------------------------------- /documentation/FAQs/i-get-a-type-instantiation-is-excessively-deep-and-potentially-infinite-error-what-should-i-do.md: -------------------------------------------------------------------------------- 1 | # I get a `type instantiation is excessively deep and potentially infinite` error, what should I do ? 2 | 3 | Though it is rare, the TS compiler can sometimes raises this error when detecting long type computations, and potential infinite loops. 4 | 5 | `FromSchema` goes through some pretty wild type recursions, so this is can be an issue on large schemas, particularly when using intersections (`allOf`) and exclusions (`not`, `else`). 6 | 7 | I am working on simplifying the type computations. But for the moment, I don't have any better solution to give you other than ignoring the error with a `@ts-ignore` comment. If the type computation is not aborted (i.e. you do not get an `any` type), the inferred type should still be valid. Otherwise, try opting out of exclusions first (`not`, `ifThenElse` keywords). 8 | 9 | If you're still having troubles, feel free to open an issue. 10 | -------------------------------------------------------------------------------- /documentation/FAQs/type-instantiation-is-excessively-deep-and-possibly-infinite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasAribart/json-schema-to-ts/116deb42b968da708cdea771d3034ce52f699f45/documentation/FAQs/type-instantiation-is-excessively-deep-and-possibly-infinite.png -------------------------------------------------------------------------------- /documentation/FAQs/will-json-schema-to-ts-impact-the-performances-of-my-ide-compiler.md: -------------------------------------------------------------------------------- 1 | # Will `json-schema-to-ts` impact the performances of my IDE/compiler? 2 | 3 | Long story short: no. 4 | 5 | In your IDE, as long as you don't define all your schemas in the same file (which you shouldn't do anyway), file opening and type infering is still fast enough for you not to hardly notice anything, even on large schemas (200+ lines). 6 | 7 | The same holds true for compilation. As far as I know (please, feel free to open an issue if you find otherwise), `json-schema-to-ts` has little to no impact on the TS compilation time. 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | globals: { 4 | "ts-jest": { 5 | isolatedModules: true, 6 | }, 7 | }, 8 | testEnvironment: "node", 9 | coverageReporters: ["json-summary"], 10 | testMatch: ["**/*.unit.test.ts"], 11 | testPathIgnorePatterns: ["/node_modules/", "/lib/"], 12 | modulePathIgnorePatterns: ["/dist/"], 13 | clearMocks: true, 14 | rootDir: "src", 15 | moduleNameMapper: { 16 | "^~/(.*)$": "/$1", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-to-ts", 3 | "version": "3.1.1", 4 | "description": "Infer typescript types from your JSON schemas!", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/esm/index.js", 10 | "types": "lib/types/index.d.ts", 11 | "scripts": { 12 | "test": "yarn test-type && yarn test-format && yarn test-unit && yarn test-unused-exports && yarn test-lint", 13 | "test-type": "tsc --noEmit", 14 | "test-format": "yarn prettier . --check", 15 | "test-unit": "jest --verbose --runInBand --collectCoverage --logHeapUsage --passWithNoTests", 16 | "test-unused-exports": "yarn ts-unused-exports ./tsconfig.json --excludePathsFromReport='src/index.ts;'", 17 | "test-lint": "yarn eslint --ext=js,ts .", 18 | "format": "yarn prettier . --write", 19 | "test-circular": "yarn depcruise --validate .dependency-cruiser.js ./src", 20 | "transpile": "babel src --extensions .ts --quiet", 21 | "build": "rm -rf lib && yarn build-cjs && yarn build-esm && yarn build-types", 22 | "build-cjs": "NODE_ENV=cjs yarn transpile --out-dir lib/cjs --source-maps", 23 | "build-esm": "NODE_ENV=esm yarn transpile --out-dir lib/esm --source-maps", 24 | "build-types": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", 25 | "set-package-version": "ts-node scripts/setPackageVersion" 26 | }, 27 | "dependencies": { 28 | "@babel/runtime": "^7.18.3", 29 | "ts-algebra": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.17.6", 33 | "@babel/core": "^7.17.5", 34 | "@babel/plugin-transform-runtime": "^7.17.0", 35 | "@babel/preset-env": "^7.16.11", 36 | "@babel/preset-typescript": "^7.16.7", 37 | "@rollup/plugin-typescript": "^8.3.2", 38 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 39 | "@types/jest": "^27.4.0", 40 | "@types/node": "^20.5.7", 41 | "@typescript-eslint/eslint-plugin": "^6.13.2", 42 | "@typescript-eslint/parser": "^6.13.2", 43 | "@zerollup/ts-transform-paths": "^1.7.18", 44 | "ajv": "^8.13.0", 45 | "babel-plugin-module-resolver": "^4.1.0", 46 | "dependency-cruiser": "^11.18.0", 47 | "eslint": "^8.27.0", 48 | "eslint-config-prettier": "^8.5.0", 49 | "eslint-import-resolver-typescript": "^3.5.2", 50 | "eslint-plugin-import": "^2.26.0", 51 | "eslint-plugin-jest": "^27.1.4", 52 | "eslint-plugin-jsdoc": "^46.4.6", 53 | "eslint-plugin-prefer-arrow": "^1.2.3", 54 | "eslint-plugin-prettier": "^5.0.1", 55 | "eslint-plugin-unused-imports": "^2.0.0", 56 | "jest": "^27.5.1", 57 | "prettier": "^3.1.0", 58 | "rollup": "^2.67.3", 59 | "rollup-plugin-dts": "4.1.0", 60 | "rollup-plugin-import-map": "^2.2.2", 61 | "rollup-plugin-typescript-paths": "^1.4.0", 62 | "ts-jest": "^28.0.2", 63 | "ts-node": "^10.9.1", 64 | "ts-toolbelt": "^9.6.0", 65 | "ts-unused-exports": "^8.0.0", 66 | "tsc-alias": "^1.8.8", 67 | "typescript": "^4.5.5" 68 | }, 69 | "engines": { 70 | "node": ">=16" 71 | }, 72 | "author": "Thomas Aribart", 73 | "license": "MIT", 74 | "repository": { 75 | "type": "git", 76 | "url": "git+https://github.com/ThomasAribart/json-schema-to-ts.git" 77 | }, 78 | "keywords": [ 79 | "json", 80 | "schema", 81 | "typescript", 82 | "type", 83 | "ts" 84 | ], 85 | "bugs": { 86 | "url": "https://github.com/ThomasAribart/json-schema-to-ts/issues" 87 | }, 88 | "homepage": "https://github.com/ThomasAribart/json-schema-to-ts#readme" 89 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import { basename, join } from "path"; 3 | import dts from "rollup-plugin-dts"; 4 | import { rollupImportMapPlugin } from "rollup-plugin-import-map"; 5 | 6 | import { dependencies } from "./package.json"; 7 | 8 | const BUILD_PATH = join("builds", "deno"); 9 | const DEFINITION_FILE_NAME = "index.d.ts"; 10 | const DEFINITION_FILE_PATH = join(BUILD_PATH, DEFINITION_FILE_NAME); 11 | const COMMONJS_FILE_NAME = "index.js"; 12 | const COMMONJS_FILE_PATH = join(BUILD_PATH, COMMONJS_FILE_NAME); 13 | const ESM_FILE_NAME = "index.mjs"; 14 | const ESM_FILE_PATH = join(BUILD_PATH, ESM_FILE_NAME); 15 | 16 | const BUNDLED_SOURCE_INPUT_PATH = join("lib", "types", "index.d.ts"); 17 | const SOURCE_INPUT_PATH = join("src", "index.ts"); 18 | const REFERENCE_COMMENT = `/// \n`; 19 | 20 | // as it currently stands, all skypack plugins for rollup do not support scoped imports (e.g. @types/*) 21 | // nor do they support a ?dts query string suffix to the url, which is necessary for deno 22 | // import maps are a great substitute for such a plugin, and they offer more flexibility 23 | const imports = {}; 24 | for (const [dep, ver] of Object.entries(dependencies)) { 25 | imports[basename(dep)] = `https://cdn.skypack.dev/${dep}@${ver}?dts`; 26 | } 27 | 28 | const config = [ 29 | { 30 | // Using bundled path to not have to deal with absolute paths transpilation 31 | input: BUNDLED_SOURCE_INPUT_PATH, 32 | output: [{ file: DEFINITION_FILE_PATH, format: "es" }], 33 | plugins: [rollupImportMapPlugin([{ imports }]), dts()], 34 | }, 35 | { 36 | input: SOURCE_INPUT_PATH, 37 | output: [ 38 | { file: COMMONJS_FILE_PATH, format: "cjs", banner: REFERENCE_COMMENT }, 39 | ], 40 | plugins: [ 41 | typescript({ 42 | compilerOptions: { lib: ["es5", "es6", "dom"], target: "es5" }, 43 | }), 44 | ], 45 | }, 46 | { 47 | input: SOURCE_INPUT_PATH, 48 | output: [{ file: ESM_FILE_PATH, format: "es", banner: REFERENCE_COMMENT }], 49 | plugins: [ 50 | typescript({ 51 | compilerOptions: { lib: ["es5", "es6", "dom"], target: "es5" }, 52 | }), 53 | ], 54 | }, 55 | ]; 56 | 57 | export default config; 58 | -------------------------------------------------------------------------------- /scripts/setPackageVersion.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { join } from "path"; 3 | 4 | const newVersionTag = process.argv[2] as string | undefined; 5 | 6 | const semanticVersioningRegex = 7 | /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; 8 | 9 | if ( 10 | newVersionTag === undefined || 11 | !semanticVersioningRegex.test(newVersionTag) 12 | ) { 13 | throw new Error("Invalid version"); 14 | } 15 | 16 | const NEW_VERSION = newVersionTag.slice(1); 17 | 18 | console.log(NEW_VERSION); 19 | 20 | type PackageJson = { 21 | version?: string; 22 | dependencies?: Record; 23 | devDependencies?: Record; 24 | peerDependencies?: Record; 25 | }; 26 | 27 | const packageJsonPath = join(__dirname, "..", "package.json"); 28 | 29 | const packageJson = JSON.parse( 30 | readFileSync(packageJsonPath).toString(), 31 | ) as PackageJson; 32 | 33 | packageJson.version = NEW_VERSION; 34 | 35 | writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 36 | -------------------------------------------------------------------------------- /src/definitions/deserializationPattern.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constraint of the FromSchema `deserialize` option 3 | */ 4 | export type DeserializationPattern = Readonly<{ 5 | pattern: unknown; 6 | output: unknown; 7 | }>; 8 | -------------------------------------------------------------------------------- /src/definitions/extendedJsonSchema.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-lines */ 2 | import type { $JSONSchema, JSONSchemaType } from "./jsonSchema"; 3 | 4 | /** 5 | * JSON Schema extension (i.e. additional custom fields) 6 | */ 7 | export type JSONSchemaExtension = Record; 8 | 9 | /** 10 | * Extended JSON Schema type constraint 11 | * @param EXTENSION JSONSchema7Extension 12 | * @returns Type 13 | */ 14 | export type ExtendedJSONSchema< 15 | EXTENSION extends JSONSchemaExtension = JSONSchemaExtension, 16 | > = 17 | | boolean 18 | | (Readonly<{ 19 | // Needed to have extended JSON schemas actually extend the JSONSchema type constraint at all time 20 | [$JSONSchema]?: $JSONSchema; 21 | 22 | $id?: string | undefined; 23 | $ref?: string | undefined; 24 | /** 25 | * Meta schema 26 | * 27 | * Recommended values: 28 | * - 'http://json-schema.org/schema#' 29 | * - 'http://json-schema.org/hyper-schema#' 30 | * - 'http://json-schema.org/draft-07/schema#' 31 | * - 'http://json-schema.org/draft-07/hyper-schema#' 32 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5 33 | */ 34 | $schema?: string | undefined; 35 | $comment?: string | undefined; 36 | 37 | /** 38 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1 39 | */ 40 | type?: JSONSchemaType | readonly JSONSchemaType[]; 41 | const?: unknown; 42 | enum?: unknown; 43 | 44 | /** 45 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2 46 | */ 47 | multipleOf?: number | undefined; 48 | maximum?: number | undefined; 49 | exclusiveMaximum?: number | undefined; 50 | minimum?: number | undefined; 51 | exclusiveMinimum?: number | undefined; 52 | 53 | /** 54 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3 55 | */ 56 | maxLength?: number | undefined; 57 | minLength?: number | undefined; 58 | pattern?: string | undefined; 59 | 60 | /** 61 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4 62 | */ 63 | items?: 64 | | ExtendedJSONSchema 65 | | readonly ExtendedJSONSchema[]; 66 | additionalItems?: ExtendedJSONSchema; 67 | contains?: ExtendedJSONSchema; 68 | maxItems?: number | undefined; 69 | minItems?: number | undefined; 70 | uniqueItems?: boolean | undefined; 71 | 72 | /** 73 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5 74 | */ 75 | maxProperties?: number | undefined; 76 | minProperties?: number | undefined; 77 | required?: readonly string[]; 78 | properties?: Readonly>>; 79 | patternProperties?: Readonly< 80 | Record> 81 | >; 82 | additionalProperties?: ExtendedJSONSchema; 83 | dependencies?: Readonly< 84 | Record | readonly string[]> 85 | >; 86 | propertyNames?: ExtendedJSONSchema; 87 | 88 | /** 89 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6 90 | */ 91 | if?: ExtendedJSONSchema; 92 | then?: ExtendedJSONSchema; 93 | else?: ExtendedJSONSchema; 94 | 95 | /** 96 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7 97 | */ 98 | allOf?: readonly ExtendedJSONSchema[]; 99 | anyOf?: readonly ExtendedJSONSchema[]; 100 | oneOf?: readonly ExtendedJSONSchema[]; 101 | not?: ExtendedJSONSchema; 102 | 103 | /** 104 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7 105 | */ 106 | format?: string | undefined; 107 | 108 | /** 109 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-8 110 | */ 111 | contentMediaType?: string | undefined; 112 | contentEncoding?: string | undefined; 113 | 114 | /** 115 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9 116 | */ 117 | definitions?: Readonly>>; 118 | 119 | /** 120 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10 121 | */ 122 | title?: string | undefined; 123 | description?: string | undefined; 124 | // Required to allow array values in default field 125 | // https://github.com/ThomasAribart/json-schema-to-ts/issues/80 126 | default?: unknown; 127 | readOnly?: boolean | undefined; 128 | writeOnly?: boolean | undefined; 129 | // Required to avoid applying Readonly to Array interface, which results in invalid type (Array is treated as Object): 130 | // https://github.com/ThomasAribart/json-schema-to-ts/issues/48 131 | // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0e40d820c92ec6457854fa6726bbff2ffea4e7dd/types/json-schema/index.d.ts#L590 132 | // https://github.com/microsoft/TypeScript/issues/3496#issuecomment-128553540 133 | examples?: readonly unknown[]; 134 | 135 | // Additional field from OpenAPI Spec, supported by JSON-Schema 136 | nullable?: boolean; 137 | }> & 138 | Readonly>); 139 | 140 | /** 141 | * Extended JSON Schema with reference type constraint 142 | * @param EXTENSION JSONSchema7Extension 143 | * @returns Type 144 | */ 145 | export type ExtendedJSONSchemaReference< 146 | EXTENSION extends JSONSchemaExtension = JSONSchemaExtension, 147 | > = ExtendedJSONSchema & Readonly<{ $id: string }>; 148 | 149 | /** 150 | * Unextends a tuple of extended JSON Schemas 151 | * @param EXTENSION JSONSchema7Extension 152 | * @param EXTENDED_SCHEMAS ExtendedJSONSchema[] 153 | * @returns ExtendedJSONSchema[] 154 | */ 155 | type UnextendJSONSchemaTuple< 156 | EXTENSION extends JSONSchemaExtension, 157 | EXTENDED_SCHEMAS extends ExtendedJSONSchema[], 158 | > = EXTENDED_SCHEMAS extends [ 159 | infer EXTENDED_SCHEMAS_HEAD, 160 | ...infer EXTENDED_SCHEMAS_TAIL, 161 | ] 162 | ? EXTENDED_SCHEMAS_HEAD extends ExtendedJSONSchema 163 | ? EXTENDED_SCHEMAS_TAIL extends ExtendedJSONSchema[] 164 | ? [ 165 | UnextendJSONSchema, 166 | ...UnextendJSONSchemaTuple, 167 | ] 168 | : never 169 | : never 170 | : []; 171 | 172 | /** 173 | * Unextends a record of extended JSON Schemas 174 | * @param EXTENSION JSONSchema7Extension 175 | * @param EXTENDED_SCHEMA_RECORD Record 176 | * @returns Record 177 | */ 178 | type UnextendJSONSchemaRecord< 179 | EXTENSION extends JSONSchemaExtension, 180 | EXTENDED_SCHEMA_RECORD extends Record, 181 | > = { 182 | [KEY in keyof EXTENDED_SCHEMA_RECORD]: EXTENDED_SCHEMA_RECORD[KEY] extends ExtendedJSONSchema 183 | ? UnextendJSONSchema 184 | : EXTENDED_SCHEMA_RECORD[KEY]; 185 | }; 186 | 187 | /** 188 | * Given an extended JSON Schema, recursively appends the `$JSONSchema7` symbol as optional property to have it actually extend the JSONSchema type constraint at all time 189 | * @param EXTENSION JSONSchema7Extension 190 | * @param EXTENDED_SCHEMA_RECORD ExtendedJSONSchema 191 | * @returns ExtendedJSONSchema 192 | */ 193 | export type UnextendJSONSchema< 194 | EXTENSION extends JSONSchemaExtension, 195 | EXTENDED_SCHEMA, 196 | > = EXTENDED_SCHEMA extends boolean 197 | ? EXTENDED_SCHEMA 198 | : { 199 | [KEY in 200 | | $JSONSchema 201 | | keyof EXTENDED_SCHEMA]: KEY extends keyof EXTENDED_SCHEMA 202 | ? EXTENDED_SCHEMA extends { [K in KEY]: ExtendedJSONSchema } 203 | ? UnextendJSONSchema 204 | : EXTENDED_SCHEMA extends { 205 | [K in KEY]: ExtendedJSONSchema[]; 206 | } 207 | ? number extends EXTENDED_SCHEMA[KEY]["length"] 208 | ? UnextendJSONSchema[] 209 | : EXTENDED_SCHEMA[KEY] extends ExtendedJSONSchema[] 210 | ? UnextendJSONSchemaTuple 211 | : never 212 | : EXTENDED_SCHEMA extends { [K in KEY]: Record } 213 | ? UnextendJSONSchemaRecord 214 | : EXTENDED_SCHEMA[KEY] 215 | : KEY extends $JSONSchema 216 | ? $JSONSchema 217 | : never; 218 | }; 219 | -------------------------------------------------------------------------------- /src/definitions/fromSchemaOptions.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchemaReference } from "~/definitions"; 2 | 3 | import type { DeserializationPattern } from "./deserializationPattern"; 4 | import type { 5 | ExtendedJSONSchemaReference, 6 | JSONSchemaExtension, 7 | } from "./extendedJsonSchema"; 8 | 9 | /** 10 | * FromSchema options constraints 11 | */ 12 | export type FromSchemaOptions = { 13 | parseNotKeyword?: boolean; 14 | parseIfThenElseKeywords?: boolean; 15 | keepDefaultedPropertiesOptional?: boolean; 16 | references?: JSONSchemaReference[] | false; 17 | deserialize?: DeserializationPattern[] | false; 18 | }; 19 | 20 | /** 21 | * FromExtendedSchema options constraints 22 | */ 23 | export type FromExtendedSchemaOptions = { 24 | parseNotKeyword?: boolean; 25 | parseIfThenElseKeywords?: boolean; 26 | keepDefaultedPropertiesOptional?: boolean; 27 | references?: ExtendedJSONSchemaReference[] | false; 28 | deserialize?: DeserializationPattern[] | false; 29 | }; 30 | 31 | /** 32 | * FromSchema default options 33 | */ 34 | export type FromSchemaDefaultOptions = { 35 | parseNotKeyword: false; 36 | parseIfThenElseKeywords: false; 37 | keepDefaultedPropertiesOptional: false; 38 | references: false; 39 | deserialize: false; 40 | }; 41 | -------------------------------------------------------------------------------- /src/definitions/index.ts: -------------------------------------------------------------------------------- 1 | export type { DeserializationPattern } from "./deserializationPattern"; 2 | export type { 3 | JSONSchema, 4 | JSONSchemaType, 5 | JSONSchemaReference, 6 | } from "./jsonSchema"; 7 | export type { 8 | FromSchemaOptions, 9 | FromExtendedSchemaOptions, 10 | FromSchemaDefaultOptions, 11 | } from "./fromSchemaOptions"; 12 | export type { 13 | JSONSchemaExtension, 14 | ExtendedJSONSchema, 15 | ExtendedJSONSchemaReference, 16 | UnextendJSONSchema, 17 | } from "./extendedJsonSchema"; 18 | -------------------------------------------------------------------------------- /src/definitions/jsonSchema.ts: -------------------------------------------------------------------------------- 1 | export const $JSONSchema = Symbol("$JSONSchema"); 2 | /** 3 | * Symbol used to make extended JSON schemas actually extend the JSONSchema type constraint at all time 4 | */ 5 | export type $JSONSchema = typeof $JSONSchema; 6 | 7 | /** 8 | * JSON Schema type 9 | */ 10 | export type JSONSchemaType = 11 | | "string" 12 | | "number" 13 | | "integer" 14 | | "boolean" 15 | | "object" 16 | | "array" 17 | | "null"; 18 | 19 | /** 20 | * JSON Schema type constraint 21 | */ 22 | export type JSONSchema = 23 | | boolean 24 | | Readonly<{ 25 | // Needed to have extended JSON schemas actually extend the JSONSchema type constraint at all time 26 | [$JSONSchema]?: $JSONSchema; 27 | 28 | $id?: string | undefined; 29 | $ref?: string | undefined; 30 | /** 31 | * Meta schema 32 | * 33 | * Recommended values: 34 | * - 'http://json-schema.org/schema#' 35 | * - 'http://json-schema.org/hyper-schema#' 36 | * - 'http://json-schema.org/draft-07/schema#' 37 | * - 'http://json-schema.org/draft-07/hyper-schema#' 38 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-5 39 | */ 40 | $schema?: string | undefined; 41 | $comment?: string | undefined; 42 | 43 | /** 44 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1 45 | */ 46 | type?: JSONSchemaType | readonly JSONSchemaType[]; 47 | const?: unknown; 48 | enum?: unknown; 49 | 50 | /** 51 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2 52 | */ 53 | multipleOf?: number | undefined; 54 | maximum?: number | undefined; 55 | exclusiveMaximum?: number | undefined; 56 | minimum?: number | undefined; 57 | exclusiveMinimum?: number | undefined; 58 | 59 | /** 60 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3 61 | */ 62 | maxLength?: number | undefined; 63 | minLength?: number | undefined; 64 | pattern?: string | undefined; 65 | 66 | /** 67 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4 68 | */ 69 | items?: JSONSchema | readonly JSONSchema[]; 70 | additionalItems?: JSONSchema; 71 | contains?: JSONSchema; 72 | maxItems?: number | undefined; 73 | minItems?: number | undefined; 74 | uniqueItems?: boolean | undefined; 75 | 76 | /** 77 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5 78 | */ 79 | maxProperties?: number | undefined; 80 | minProperties?: number | undefined; 81 | required?: readonly string[]; 82 | properties?: Readonly>; 83 | patternProperties?: Readonly>; 84 | additionalProperties?: JSONSchema; 85 | unevaluatedProperties?: JSONSchema; 86 | dependencies?: Readonly>; 87 | propertyNames?: JSONSchema; 88 | 89 | /** 90 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6 91 | */ 92 | if?: JSONSchema; 93 | then?: JSONSchema; 94 | else?: JSONSchema; 95 | 96 | /** 97 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7 98 | */ 99 | allOf?: readonly JSONSchema[]; 100 | anyOf?: readonly JSONSchema[]; 101 | oneOf?: readonly JSONSchema[]; 102 | not?: JSONSchema; 103 | 104 | /** 105 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7 106 | */ 107 | format?: string | undefined; 108 | 109 | /** 110 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-8 111 | */ 112 | contentMediaType?: string | undefined; 113 | contentEncoding?: string | undefined; 114 | 115 | /** 116 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9 117 | */ 118 | definitions?: Readonly>; 119 | 120 | /** 121 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10 122 | */ 123 | title?: string | undefined; 124 | description?: string | undefined; 125 | // Required to allow array values in default field 126 | // https://github.com/ThomasAribart/json-schema-to-ts/issues/80 127 | default?: unknown; 128 | readOnly?: boolean | undefined; 129 | writeOnly?: boolean | undefined; 130 | // Required to avoid applying Readonly to Array interface, which results in invalid type (Array is treated as Object): 131 | // https://github.com/ThomasAribart/json-schema-to-ts/issues/48 132 | // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0e40d820c92ec6457854fa6726bbff2ffea4e7dd/types/json-schema/index.d.ts#L590 133 | // https://github.com/microsoft/TypeScript/issues/3496#issuecomment-128553540 134 | examples?: readonly unknown[]; 135 | 136 | // Additional field from OpenAPI Spec, supported by JSON-Schema 137 | nullable?: boolean; 138 | }>; 139 | 140 | /** 141 | * JSON Schema with reference type constraint 142 | */ 143 | export type JSONSchemaReference = JSONSchema & Readonly<{ $id: string }>; 144 | -------------------------------------------------------------------------------- /src/definitions/jsonSchema.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema } from "~/index"; 2 | 3 | // Should work with array examples 4 | const schemaWithArrayExamples: JSONSchema = { 5 | additionalProperties: true, 6 | type: "object", 7 | properties: { 8 | foo: { type: "string" }, 9 | }, 10 | examples: [ 11 | { 12 | foo: "bar", 13 | someInt: 1, 14 | someArray: [], 15 | }, 16 | ], 17 | } as const; 18 | schemaWithArrayExamples; 19 | 20 | // Should work with non-array default 21 | const schemaWithNonArrayDefault: JSONSchema = { 22 | additionalProperties: true, 23 | type: "object", 24 | properties: { 25 | foo: { 26 | type: "array", 27 | items: { type: "string" }, 28 | default: "thomas", 29 | }, 30 | }, 31 | } as const; 32 | schemaWithNonArrayDefault; 33 | 34 | // Should work with array default 35 | const schemaWithArrayDefault: JSONSchema = { 36 | additionalProperties: true, 37 | type: "object", 38 | properties: { 39 | foo: { 40 | type: "array", 41 | items: { type: "string" }, 42 | default: ["thomas", "stan"], 43 | }, 44 | }, 45 | } as const; 46 | schemaWithArrayDefault; 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { 4 | ExtendedJSONSchema, 5 | FromExtendedSchemaOptions, 6 | FromSchemaDefaultOptions, 7 | FromSchemaOptions, 8 | JSONSchema, 9 | JSONSchemaExtension, 10 | UnextendJSONSchema, 11 | } from "./definitions"; 12 | import type { ParseOptions } from "./parse-options"; 13 | import type { ParseSchema } from "./parse-schema"; 14 | 15 | export type { 16 | ExtendedJSONSchema, 17 | DeserializationPattern, 18 | FromSchemaOptions, 19 | FromExtendedSchemaOptions, 20 | FromSchemaDefaultOptions, 21 | JSONSchemaExtension, 22 | JSONSchema, 23 | } from "./definitions"; 24 | export type { $Compiler, Compiler, $Validator, Validator } from "./utils"; 25 | export { 26 | wrapCompilerAsTypeGuard, 27 | wrapValidatorAsTypeGuard, 28 | asConst, 29 | } from "./utils"; 30 | 31 | /** 32 | * Given a JSON schema defined with the `as const` statement, infers the type of valid instances 33 | * @param SCHEMA JSON schema 34 | */ 35 | export type FromSchema< 36 | SCHEMA extends JSONSchema, 37 | OPTIONS extends FromSchemaOptions = FromSchemaDefaultOptions, 38 | > = M.$Resolve>>; 39 | 40 | /** 41 | * Given an extended JSON schema defined with the `as const` statement, infers the type of valid instances 42 | * @param SCHEMA JSON schema 43 | */ 44 | export type FromExtendedSchema< 45 | EXTENSION extends JSONSchemaExtension, 46 | SCHEMA extends ExtendedJSONSchema, 47 | OPTIONS extends 48 | FromExtendedSchemaOptions = FromSchemaDefaultOptions, 49 | UNEXTENDED_SCHEMA = UnextendJSONSchema, 50 | > = UNEXTENDED_SCHEMA extends JSONSchema 51 | ? FromSchema 52 | : never; 53 | -------------------------------------------------------------------------------- /src/parse-options.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DeserializationPattern, 3 | FromSchemaDefaultOptions, 4 | FromSchemaOptions, 5 | JSONSchema, 6 | JSONSchemaReference, 7 | } from "./definitions"; 8 | 9 | /** 10 | * Index schema references by their $id property and make them writable. 11 | * @param SCHEMA_REFERENCES JSONSchemaReference[] 12 | * @returns Record 13 | */ 14 | export type IndexReferencesById< 15 | SCHEMA_REFERENCES extends readonly JSONSchemaReference[], 16 | > = { 17 | [REF_SCHEMA in SCHEMA_REFERENCES[number] as REF_SCHEMA["$id"]]: REF_SCHEMA; 18 | }; 19 | 20 | /** 21 | * Slightly transforms `FromSchema` options to valid `ParseSchema` options. 22 | * @param SCHEMA_REFERENCES JSONSchemaReference[] 23 | * @param OPTIONS FromSchemaOptions 24 | * @returns ParseSchemaOptions 25 | */ 26 | export type ParseOptions< 27 | ROOT_SCHEMA extends JSONSchema, 28 | OPTIONS extends FromSchemaOptions, 29 | > = { 30 | parseNotKeyword: OPTIONS["parseNotKeyword"] extends boolean 31 | ? OPTIONS["parseNotKeyword"] 32 | : FromSchemaDefaultOptions["parseNotKeyword"]; 33 | parseIfThenElseKeywords: OPTIONS["parseIfThenElseKeywords"] extends boolean 34 | ? OPTIONS["parseIfThenElseKeywords"] 35 | : FromSchemaDefaultOptions["parseIfThenElseKeywords"]; 36 | keepDefaultedPropertiesOptional: OPTIONS["keepDefaultedPropertiesOptional"] extends boolean 37 | ? OPTIONS["keepDefaultedPropertiesOptional"] 38 | : FromSchemaDefaultOptions["keepDefaultedPropertiesOptional"]; 39 | rootSchema: ROOT_SCHEMA; 40 | references: OPTIONS["references"] extends JSONSchemaReference[] 41 | ? IndexReferencesById 42 | : {}; 43 | deserialize: OPTIONS["deserialize"] extends DeserializationPattern[] | false 44 | ? OPTIONS["deserialize"] 45 | : FromSchemaDefaultOptions["deserialize"]; 46 | }; 47 | -------------------------------------------------------------------------------- /src/parse-options.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchemaDefaultOptions } from "~/definitions"; 4 | 5 | import type { IndexReferencesById, ParseOptions } from "./parse-options"; 6 | 7 | // ParseReferences 8 | 9 | type StringReference = { $id: "string"; type: "string" }; 10 | type NumberReference = { $id: "number"; type: "number" }; 11 | type ObjectReference = { $id: "object"; type: "object" }; 12 | type AllReferences = [StringReference, NumberReference, ObjectReference]; 13 | 14 | type ReceivedReferences = IndexReferencesById; 15 | type ExpectedReferences = { 16 | string: StringReference; 17 | number: NumberReference; 18 | object: ObjectReference; 19 | }; 20 | 21 | const assertReferences: A.Equals = 1; 22 | assertReferences; 23 | 24 | // ParseOptions 25 | 26 | type RootSchema = {}; 27 | 28 | type ReceivedOptions = ParseOptions; 29 | type ExpectedOptions = { 30 | parseNotKeyword: FromSchemaDefaultOptions["parseNotKeyword"]; 31 | parseIfThenElseKeywords: FromSchemaDefaultOptions["parseIfThenElseKeywords"]; 32 | keepDefaultedPropertiesOptional: FromSchemaDefaultOptions["keepDefaultedPropertiesOptional"]; 33 | deserialize: FromSchemaDefaultOptions["deserialize"]; 34 | rootSchema: RootSchema; 35 | references: IndexReferencesById; 36 | }; 37 | 38 | const assertOptions: A.Equals = 1; 39 | assertOptions; 40 | -------------------------------------------------------------------------------- /src/parse-schema/ajv.util.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv2019 from "ajv/dist/2019"; 2 | 3 | export const ajv = new Ajv2019({ strict: false }); 4 | -------------------------------------------------------------------------------- /src/parse-schema/allOf.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | import type { MergeSubSchema } from "./utils"; 7 | 8 | /** 9 | * JSON schemas of JSON schema intersections 10 | * @example 11 | * const intersectionSchema = { 12 | * allOf: [ 13 | * { type: "number" }, 14 | * { enum: [1, 2, "foo"] } 15 | * ] 16 | * } 17 | */ 18 | export type AllOfSchema = JSONSchema & 19 | Readonly<{ allOf: readonly JSONSchema[] }>; 20 | 21 | /** 22 | * Recursively parses a JSON schema intersection to a meta-type. 23 | * 24 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 25 | * @param ALL_OF_SCHEMA JSONSchema (exclusive schema union) 26 | * @param OPTIONS Parsing options 27 | * @returns Meta-type 28 | */ 29 | export type ParseAllOfSchema< 30 | ALL_OF_SCHEMA extends AllOfSchema, 31 | OPTIONS extends ParseSchemaOptions, 32 | > = RecurseOnAllOfSchema< 33 | ALL_OF_SCHEMA["allOf"], 34 | ALL_OF_SCHEMA, 35 | OPTIONS, 36 | ParseSchema, OPTIONS> 37 | >; 38 | 39 | /** 40 | * Recursively parses a tuple of JSON schemas to the intersection of its parsed meta-types (merged with root schema). 41 | * @param SUB_SCHEMAS JSONSchema[] 42 | * @param ROOT_ALL_OF_SCHEMA Root JSONSchema (schema union) 43 | * @param OPTIONS Parsing options 44 | * @returns Meta-type 45 | */ 46 | type RecurseOnAllOfSchema< 47 | SUB_SCHEMAS extends readonly JSONSchema[], 48 | ROOT_ALL_OF_SCHEMA extends AllOfSchema, 49 | OPTIONS extends ParseSchemaOptions, 50 | PARSED_ROOT_ALL_OF_SCHEMA, 51 | > = SUB_SCHEMAS extends readonly [ 52 | infer SUB_SCHEMAS_HEAD, 53 | ...infer SUB_SCHEMAS_TAIL, 54 | ] 55 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 56 | SUB_SCHEMAS_HEAD extends JSONSchema 57 | ? SUB_SCHEMAS_TAIL extends readonly JSONSchema[] 58 | ? RecurseOnAllOfSchema< 59 | SUB_SCHEMAS_TAIL, 60 | ROOT_ALL_OF_SCHEMA, 61 | OPTIONS, 62 | M.$Intersect< 63 | ParseSchema< 64 | MergeSubSchema< 65 | Omit, 66 | SUB_SCHEMAS_HEAD 67 | >, 68 | OPTIONS 69 | >, 70 | PARSED_ROOT_ALL_OF_SCHEMA 71 | > 72 | > 73 | : never 74 | : never 75 | : PARSED_ROOT_ALL_OF_SCHEMA; 76 | -------------------------------------------------------------------------------- /src/parse-schema/anyOf.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | import type { MergeSubSchema } from "./utils"; 7 | 8 | /** 9 | * JSON schemas of JSON schema unions 10 | * @example 11 | * const unionSchema = { 12 | * anyOf: [ 13 | * { type: "number" }, 14 | * { type: "string" } 15 | * ] 16 | * } 17 | */ 18 | export type AnyOfSchema = JSONSchema & 19 | Readonly<{ anyOf: readonly JSONSchema[] }>; 20 | 21 | /** 22 | * Recursively parses a JSON schema union to a meta-type. 23 | * 24 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 25 | * @param ANY_OF_SCHEMA JSONSchema (schema union) 26 | * @param OPTIONS Parsing options 27 | * @returns Meta-type 28 | */ 29 | export type ParseAnyOfSchema< 30 | ANY_OF_SCHEMA extends AnyOfSchema, 31 | OPTIONS extends ParseSchemaOptions, 32 | > = M.$Union< 33 | RecurseOnAnyOfSchema 34 | >; 35 | 36 | /** 37 | * Recursively parses a tuple of JSON schemas to the union of its parsed meta-types (merged with root schema). 38 | * @param SUB_SCHEMAS JSONSchema[] 39 | * @param ROOT_ANY_OF_SCHEMA Root JSONSchema (schema union) 40 | * @param OPTIONS Parsing options 41 | * @returns Meta-type 42 | */ 43 | type RecurseOnAnyOfSchema< 44 | SUB_SCHEMAS extends readonly JSONSchema[], 45 | ROOT_ANY_OF_SCHEMA extends AnyOfSchema, 46 | OPTIONS extends ParseSchemaOptions, 47 | RESULT = never, 48 | > = SUB_SCHEMAS extends readonly [ 49 | infer SUB_SCHEMAS_HEAD, 50 | ...infer SUB_SCHEMAS_TAIL, 51 | ] 52 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 53 | SUB_SCHEMAS_HEAD extends JSONSchema 54 | ? SUB_SCHEMAS_TAIL extends readonly JSONSchema[] 55 | ? RecurseOnAnyOfSchema< 56 | SUB_SCHEMAS_TAIL, 57 | ROOT_ANY_OF_SCHEMA, 58 | OPTIONS, 59 | | RESULT 60 | | M.$Intersect< 61 | ParseSchema, OPTIONS>, 62 | ParseSchema< 63 | MergeSubSchema< 64 | Omit, 65 | SUB_SCHEMAS_HEAD 66 | >, 67 | OPTIONS 68 | > 69 | > 70 | > 71 | : never 72 | : never 73 | : RESULT; 74 | -------------------------------------------------------------------------------- /src/parse-schema/const.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | import type { Writable } from "~/type-utils"; 5 | 6 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 7 | 8 | /** 9 | * JSON schemas of constants (i.e. types with cardinalities of 1) 10 | * @example 11 | * const constSchema = { 12 | * const: "foo" 13 | * } 14 | */ 15 | export type ConstSchema = JSONSchema & Readonly<{ const: unknown }>; 16 | 17 | /** 18 | * Recursively parses a constant JSON schema to a meta-type. 19 | * 20 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 21 | * @param CONST_SCHEMA JSONSchema (constant) 22 | * @param OPTIONS Parsing options 23 | * @returns Meta-type 24 | */ 25 | export type ParseConstSchema< 26 | CONST_SCHEMA extends ConstSchema, 27 | OPTIONS extends ParseSchemaOptions, 28 | > = M.$Intersect< 29 | ParseConst, 30 | ParseSchema, OPTIONS> 31 | >; 32 | 33 | /** 34 | * Parses a constant JSON schema to a meta-type. 35 | * @param CONST_SCHEMA JSONSchema (constant) 36 | * @param OPTIONS Parsing options 37 | * @returns Meta-type 38 | */ 39 | type ParseConst = M.Const< 40 | Writable 41 | >; 42 | -------------------------------------------------------------------------------- /src/parse-schema/const.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Const schemas", () => { 6 | describe("Null", () => { 7 | const null1Schema = { 8 | const: null, 9 | } as const; 10 | 11 | const null2Schema = { 12 | type: "null", 13 | const: null, 14 | } as const; 15 | 16 | type Null1 = FromSchema; 17 | let null1Instance: Null1; 18 | 19 | type Null2 = FromSchema; 20 | let null2Instance: Null2; 21 | 22 | it("accepts null", () => { 23 | null1Instance = null; 24 | expect(ajv.validate(null1Schema, null1Instance)).toBe(true); 25 | 26 | null2Instance = null; 27 | expect(ajv.validate(null2Schema, null2Instance)).toBe(true); 28 | }); 29 | 30 | it("rejects other values", () => { 31 | // @ts-expect-error 32 | null1Instance = "not null"; 33 | expect(ajv.validate(null1Schema, null1Instance)).toBe(false); 34 | 35 | // @ts-expect-error 36 | null2Instance = 42; 37 | expect(ajv.validate(null2Schema, null2Instance)).toBe(false); 38 | }); 39 | 40 | it('returns never if "type" and "const" are incompatible', () => { 41 | const invalidSchema = { 42 | type: "string", 43 | const: null, 44 | } as const; 45 | 46 | type Never = FromSchema; 47 | let neverInstance: Never; 48 | 49 | // @ts-expect-error 50 | neverInstance = null; 51 | expect(ajv.validate(false, neverInstance)).toBe(false); 52 | 53 | // @ts-expect-error 54 | neverInstance = true; 55 | expect(ajv.validate(false, neverInstance)).toBe(false); 56 | 57 | // @ts-expect-error 58 | neverInstance = "string"; 59 | expect(ajv.validate(false, neverInstance)).toBe(false); 60 | 61 | // @ts-expect-error 62 | neverInstance = 42; 63 | expect(ajv.validate(false, neverInstance)).toBe(false); 64 | 65 | // @ts-expect-error 66 | neverInstance = { foo: "bar" }; 67 | expect(ajv.validate(false, neverInstance)).toBe(false); 68 | 69 | // @ts-expect-error 70 | neverInstance = ["foo", "bar"]; 71 | expect(ajv.validate(false, neverInstance)).toBe(false); 72 | }); 73 | }); 74 | }); 75 | 76 | describe("Boolean", () => { 77 | it("only validates true", () => { 78 | const trueSchema = { 79 | const: true, 80 | } as const; 81 | 82 | type True = FromSchema; 83 | let trueInstance: True; 84 | 85 | trueInstance = true; 86 | expect(ajv.validate(trueSchema, trueInstance)).toBe(true); 87 | 88 | // @ts-expect-error 89 | trueInstance = false; 90 | expect(ajv.validate(trueSchema, trueInstance)).toBe(false); 91 | 92 | // @ts-expect-error 93 | trueInstance = "true"; 94 | expect(ajv.validate(trueSchema, trueInstance)).toBe(false); 95 | }); 96 | 97 | it("should only validate false (with added type)", () => { 98 | const falseSchema = { 99 | type: "boolean", 100 | const: false, 101 | } as const; 102 | 103 | type False = FromSchema; 104 | let falseInstance: False; 105 | 106 | falseInstance = false; 107 | expect(ajv.validate(falseSchema, falseInstance)).toBe(true); 108 | 109 | // @ts-expect-error 110 | falseInstance = true; 111 | expect(ajv.validate(falseSchema, falseInstance)).toBe(false); 112 | 113 | // @ts-expect-error 114 | falseInstance = "false"; 115 | expect(ajv.validate(falseSchema, falseInstance)).toBe(false); 116 | }); 117 | }); 118 | 119 | describe("String", () => { 120 | const applesSchema = { 121 | const: "apples", 122 | } as const; 123 | 124 | type Apples = FromSchema; 125 | let applesInstance: Apples; 126 | 127 | it("accepts 'apples'", () => { 128 | applesInstance = "apples"; 129 | expect(ajv.validate(applesSchema, applesInstance)).toBe(true); 130 | }); 131 | 132 | it("rejects invalid string", () => { 133 | // @ts-expect-error 134 | applesInstance = "tomatoes"; 135 | expect(ajv.validate(applesSchema, applesInstance)).toBe(false); 136 | }); 137 | 138 | it("rejects other values", () => { 139 | // @ts-expect-error 140 | applesInstance = 42; 141 | expect(ajv.validate(applesSchema, applesInstance)).toBe(false); 142 | }); 143 | 144 | const tomatoesSchema = { 145 | type: "string", 146 | const: "tomatoes", 147 | } as const; 148 | 149 | type Tomatoes = FromSchema; 150 | let tomatoesInstance: Tomatoes; 151 | 152 | it("accepts 'tomatoes' (with added type)", () => { 153 | tomatoesInstance = "tomatoes"; 154 | expect(ajv.validate(tomatoesSchema, tomatoesInstance)).toBe(true); 155 | }); 156 | 157 | it("rejects invalid string (with added type)", () => { 158 | // @ts-expect-error 159 | tomatoesInstance = "apples"; 160 | expect(ajv.validate(tomatoesSchema, tomatoesInstance)).toBe(false); 161 | }); 162 | }); 163 | 164 | describe("Integer", () => { 165 | const fortyTwoSchema = { 166 | const: 42, 167 | } as const; 168 | 169 | type FortyTwo = FromSchema; 170 | let fortyTwoInstance: FortyTwo; 171 | 172 | it("accepts 42", () => { 173 | fortyTwoInstance = 42; 174 | expect(ajv.validate(fortyTwoSchema, fortyTwoInstance)).toBe(true); 175 | }); 176 | 177 | it("rejects other number", () => { 178 | // @ts-expect-error 179 | fortyTwoInstance = 43; 180 | expect(ajv.validate(fortyTwoSchema, fortyTwoInstance)).toBe(false); 181 | }); 182 | 183 | it("rejects other values", () => { 184 | // @ts-expect-error 185 | fortyTwoInstance = "42"; 186 | expect(ajv.validate(fortyTwoSchema, fortyTwoInstance)).toBe(false); 187 | }); 188 | }); 189 | 190 | describe("Object", () => { 191 | const dogoSchema = { 192 | const: { name: "Dogo", age: 13, hobbies: ["barking", "urinating"] }, 193 | } as const; 194 | 195 | type Dogo = FromSchema; 196 | let dogoInstance: Dogo; 197 | 198 | it("accepts correct object", () => { 199 | dogoInstance = { name: "Dogo", age: 13, hobbies: ["barking", "urinating"] }; 200 | expect(ajv.validate(dogoSchema, dogoInstance)).toBe(true); 201 | }); 202 | 203 | it("rejects invalid object", () => { 204 | // @ts-expect-error 205 | dogoInstance = { name: "Doga", age: 13, hobbies: ["barking", "urinating"] }; 206 | expect(ajv.validate(dogoSchema, dogoInstance)).toBe(false); 207 | }); 208 | }); 209 | 210 | describe("Array", () => { 211 | const pizzaRecipe = [ 212 | 1, 213 | "pizza", 214 | { description: "A delicious pizza" }, 215 | ["tomatoes", "cheese"], 216 | ] as [ 217 | 1, 218 | "pizza", 219 | { description: "A delicious pizza" }, 220 | ["tomatoes", "cheese"], 221 | ]; 222 | 223 | const pizzaRecipeSchema = { 224 | const: pizzaRecipe, 225 | } as const; 226 | 227 | type PizzaRecipe = FromSchema; 228 | let pizzaRecipeInstance: PizzaRecipe; 229 | 230 | it("accepts valid array", () => { 231 | pizzaRecipeInstance = pizzaRecipe; 232 | expect(ajv.validate(pizzaRecipeSchema, pizzaRecipeInstance)).toBe(true); 233 | }); 234 | 235 | it("rejects invalid array", () => { 236 | pizzaRecipeInstance = [ 237 | // @ts-expect-error 238 | 2, 239 | "pizza", 240 | { description: "A delicious pizza" }, 241 | ["tomatoes", "cheese"], 242 | ]; 243 | expect(ajv.validate(pizzaRecipeSchema, pizzaRecipeInstance)).toBe(false); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /src/parse-schema/deserialize.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { DeserializationPattern, JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchemaOptions } from "./index"; 6 | 7 | /** 8 | * Apply deserialization patterns to a JSON schema 9 | * @param SCHEMA JSONSchema 10 | * @param OPTIONS Parsing options (with deserialization patterns) 11 | * @returns Meta-type 12 | */ 13 | export type DeserializeSchema< 14 | SCHEMA extends JSONSchema, 15 | OPTIONS extends Omit & { 16 | deserialize: DeserializationPattern[]; 17 | }, 18 | > = RecurseOnDeserializationPatterns; 19 | 20 | /** 21 | * Recursively apply deserialization patterns to a JSON schema 22 | * @param SCHEMA JSONSchema 23 | * @param DESERIALIZATION_PATTERNS DeserializationPattern[] 24 | * @returns Meta-type 25 | */ 26 | type RecurseOnDeserializationPatterns< 27 | SCHEMA extends JSONSchema, 28 | DESERIALIZATION_PATTERNS extends DeserializationPattern[], 29 | RESULT = M.Any, 30 | > = DESERIALIZATION_PATTERNS extends [ 31 | infer DESERIALIZATION_PATTERNS_HEAD, 32 | ...infer DESERIALIZATION_PATTERNS_TAIL, 33 | ] 34 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 35 | DESERIALIZATION_PATTERNS_HEAD extends DeserializationPattern 36 | ? DESERIALIZATION_PATTERNS_TAIL extends DeserializationPattern[] 37 | ? RecurseOnDeserializationPatterns< 38 | SCHEMA, 39 | DESERIALIZATION_PATTERNS_TAIL, 40 | SCHEMA extends DESERIALIZATION_PATTERNS_HEAD["pattern"] 41 | ? M.$Intersect< 42 | M.Any, 43 | RESULT 44 | > 45 | : RESULT 46 | > 47 | : never 48 | : never 49 | : RESULT; 50 | -------------------------------------------------------------------------------- /src/parse-schema/deserialize.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | import type { A } from "ts-toolbelt"; 3 | 4 | import type { 5 | ExtendedJSONSchema, 6 | FromExtendedSchema, 7 | FromSchema, 8 | } from "~/index"; 9 | import type { ParseOptions } from "~/parse-options"; 10 | 11 | import type { DeserializeSchema } from "./deserialize"; 12 | 13 | type DatePattern = { type: "string"; format: "date-time" }; 14 | type DeserializeDates = { pattern: DatePattern; output: Date }; 15 | 16 | type BrandedPattern = { type: "string"; branded: "event-id" }; 17 | type DeserializeBranded = { 18 | pattern: BrandedPattern; 19 | output: { brand: "event-id" }; 20 | }; 21 | 22 | // Non-serialized 23 | 24 | type StringSchema = { type: "string" }; 25 | 26 | const parseNonSerialized: A.Equals< 27 | DeserializeSchema< 28 | StringSchema, 29 | ParseOptions 30 | >, 31 | M.Any 32 | > = 1; 33 | parseNonSerialized; 34 | 35 | const assertNonSerialized: A.Equals< 36 | FromSchema, 37 | string 38 | > = 1; 39 | assertNonSerialized; 40 | 41 | // Serialized string 42 | 43 | const parseSerialized: A.Equals< 44 | DeserializeSchema< 45 | DatePattern, 46 | ParseOptions 47 | >, 48 | M.Any 49 | > = 1; 50 | parseSerialized; 51 | 52 | const assertSerialized: A.Equals< 53 | FromSchema, 54 | Date 55 | > = 1; 56 | assertSerialized; 57 | 58 | // Deep serialized string 59 | 60 | type DeepSerializedSchema = { 61 | type: "object"; 62 | properties: { 63 | date: DatePattern; 64 | }; 65 | required: ["date"]; 66 | additionalProperties: false; 67 | }; 68 | 69 | const parseDeepNonSerialized: A.Equals< 70 | DeserializeSchema< 71 | DeepSerializedSchema, 72 | ParseOptions 73 | >, 74 | M.Any 75 | > = 1; 76 | parseDeepNonSerialized; 77 | 78 | const assertDeepSerialized: A.Equals< 79 | FromSchema, 80 | { date: Date } 81 | > = 1; 82 | assertDeepSerialized; 83 | 84 | // Double serialized string 85 | 86 | type DoublePattern = DatePattern & BrandedPattern; 87 | 88 | const parseDoubleSerialized: A.Equals< 89 | DeserializeSchema< 90 | DoublePattern, 91 | ParseOptions< 92 | DoublePattern, 93 | { deserialize: [DeserializeDates, DeserializeBranded] } 94 | > 95 | >, 96 | M.Any 97 | > = 1; 98 | parseDoubleSerialized; 99 | 100 | const assertDoubleSerialized: A.Equals< 101 | FromSchema< 102 | DoublePattern, 103 | { deserialize: [DeserializeDates, DeserializeBranded] } 104 | >, 105 | Date & { brand: "event-id" } 106 | > = 1; 107 | assertDoubleSerialized; 108 | 109 | // Extended 110 | 111 | type Extension = { customId: string }; 112 | type Extended = ExtendedJSONSchema; 113 | 114 | const extendedSchema: Extended = { 115 | customId: "foo", 116 | }; 117 | extendedSchema; 118 | 119 | const extendedObjectSchema: Extended = { 120 | type: "object", 121 | properties: { 122 | foo: { customId: "foo" }, 123 | }, 124 | }; 125 | extendedObjectSchema; 126 | 127 | const invalidSchema: Extended = { 128 | // @ts-expect-error 129 | badId: "bar", 130 | }; 131 | invalidSchema; 132 | 133 | const assertExtendedSerialized: A.Equals< 134 | FromExtendedSchema< 135 | Extension, 136 | { 137 | type: "object"; 138 | properties: { 139 | foo: { customId: "foo" }; 140 | bar: { customId: "bar" }; 141 | }; 142 | required: ["foo", "bar"]; 143 | additionalProperties: false; 144 | }, 145 | { 146 | deserialize: [ 147 | { 148 | pattern: { customId: "foo" }; 149 | output: string; 150 | }, 151 | { 152 | pattern: { customId: "bar" }; 153 | output: number; 154 | }, 155 | ]; 156 | } 157 | >, 158 | { foo: string; bar: number } 159 | > = 1; 160 | assertExtendedSerialized; 161 | -------------------------------------------------------------------------------- /src/parse-schema/enum.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | import type { Writable } from "~/type-utils"; 5 | 6 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 7 | 8 | /** 9 | * JSON schemas of enums (i.e. types with finite cardinalities) 10 | * @example 11 | * const enumSchema = { 12 | * enum: ["foo", "bar"] 13 | * } 14 | */ 15 | export type EnumSchema = JSONSchema & Readonly<{ enum: readonly unknown[] }>; 16 | 17 | /** 18 | * Recursively parses an enum JSON schema to a meta-type. 19 | * 20 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 21 | * @param ENUM_SCHEMA JSONSchema (enum) 22 | * @param OPTIONS Parsing options 23 | * @returns Meta-type 24 | */ 25 | export type ParseEnumSchema< 26 | ENUM_SCHEMA extends EnumSchema, 27 | OPTIONS extends ParseSchemaOptions, 28 | > = M.$Intersect< 29 | ParseEnum, 30 | ParseSchema, OPTIONS> 31 | >; 32 | 33 | /** 34 | * Parses an enum JSON schema to a meta-type. 35 | * @param ENUM_SCHEMA JSONSchema (enum) 36 | * @param OPTIONS Parsing options 37 | * @returns Meta-type 38 | */ 39 | type ParseEnum = M.Enum< 40 | Writable 41 | >; 42 | -------------------------------------------------------------------------------- /src/parse-schema/enum.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Enum schemas", () => { 6 | describe("Empty enum", () => { 7 | const emptyEnumSchema = { enum: [] } as const; 8 | 9 | type Never = FromSchema; 10 | let neverInstance: Never; 11 | 12 | it("rejects any value", () => { 13 | // @ts-expect-error 14 | neverInstance = null; 15 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 16 | 17 | // @ts-expect-error 18 | neverInstance = true; 19 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 20 | 21 | // @ts-expect-error 22 | neverInstance = "string"; 23 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 24 | 25 | // @ts-expect-error 26 | neverInstance = 42; 27 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 28 | 29 | // @ts-expect-error 30 | neverInstance = { foo: "bar" }; 31 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 32 | 33 | // @ts-expect-error 34 | neverInstance = ["foo", "bar"]; 35 | expect(() => ajv.validate(emptyEnumSchema, neverInstance)).toThrow(); 36 | }); 37 | }); 38 | 39 | describe("Mixed", () => { 40 | const mixedEnumSchema = { 41 | enum: [null, true, 12, "tomatoe", { name: "dogo" }, ["foo", "bar"]], 42 | } as const; 43 | 44 | type Mixed = FromSchema; 45 | let mixedInstance: Mixed; 46 | 47 | it("accepts any listed value", () => { 48 | mixedInstance = null; 49 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 50 | 51 | mixedInstance = true; 52 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 53 | 54 | mixedInstance = 12; 55 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 56 | 57 | mixedInstance = "tomatoe"; 58 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 59 | 60 | mixedInstance = { name: "dogo" }; 61 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 62 | 63 | mixedInstance = ["foo", "bar"]; 64 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(true); 65 | }); 66 | 67 | it("rejects other values", () => { 68 | // @ts-expect-error 69 | mixedInstance = false; 70 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(false); 71 | 72 | // @ts-expect-error 73 | mixedInstance = 13; 74 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(false); 75 | 76 | // @ts-expect-error 77 | mixedInstance = "apples"; 78 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(false); 79 | 80 | // @ts-expect-error 81 | mixedInstance = { name: "dingo" }; 82 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(false); 83 | 84 | // @ts-expect-error 85 | mixedInstance = ["foo", "baz"]; 86 | expect(ajv.validate(mixedEnumSchema, mixedInstance)).toBe(false); 87 | }); 88 | }); 89 | 90 | describe("String enum", () => { 91 | const fruitEnumSchema = { 92 | type: "string", 93 | enum: ["apples", "tomatoes", "bananas", true], 94 | } as const; 95 | 96 | type Fruit = FromSchema; 97 | let fruitInstance: Fruit; 98 | 99 | it("accepts valid string", () => { 100 | fruitInstance = "apples"; 101 | expect(ajv.validate(fruitEnumSchema, fruitInstance)).toBe(true); 102 | 103 | fruitInstance = "tomatoes"; 104 | expect(ajv.validate(fruitEnumSchema, fruitInstance)).toBe(true); 105 | 106 | fruitInstance = "bananas"; 107 | expect(ajv.validate(fruitEnumSchema, fruitInstance)).toBe(true); 108 | }); 109 | 110 | it("rejects other values", () => { 111 | // @ts-expect-error 112 | fruitInstance = true; 113 | expect(ajv.validate(fruitEnumSchema, true)).toBe(false); 114 | 115 | // @ts-expect-error 116 | fruitInstance = 42; 117 | expect(ajv.validate(fruitEnumSchema, true)).toBe(false); 118 | }); 119 | }); 120 | 121 | describe("Number enum", () => { 122 | const numberEnumSchema = { 123 | type: "number", 124 | enum: [13, 42, { not: "a number" }], 125 | } as const; 126 | 127 | type Number = FromSchema; 128 | let numberInstance: Number; 129 | 130 | it("accepts valid number", () => { 131 | numberInstance = 13; 132 | expect(ajv.validate(numberEnumSchema, numberInstance)).toBe(true); 133 | 134 | numberInstance = 42; 135 | expect(ajv.validate(numberEnumSchema, numberInstance)).toBe(true); 136 | }); 137 | 138 | it("rejects other values", () => { 139 | // @ts-expect-error 140 | numberInstance = { not: "a number" }; 141 | expect(ajv.validate(numberEnumSchema, numberInstance)).toBe(false); 142 | }); 143 | }); 144 | 145 | describe("Tuple enum", () => { 146 | const recipeEnumSchema = { 147 | type: "array", 148 | enum: [ 149 | [0, "pasta", { descr: "Italian meal" }], 150 | [1, "pizza", { descr: "A delicious pizza" }, ["tomatoes", "cheese"]], 151 | { not: "a recipe" }, 152 | ], 153 | } as const; 154 | 155 | type Recipe = FromSchema; 156 | let recipe: Recipe; 157 | 158 | it("accepts valid tuples", () => { 159 | recipe = [0, "pasta", { descr: "Italian meal" }]; 160 | expect(ajv.validate(recipeEnumSchema, recipe)).toBe(true); 161 | 162 | recipe = [ 163 | 1, 164 | "pizza", 165 | { descr: "A delicious pizza" }, 166 | ["tomatoes", "cheese"], 167 | ]; 168 | expect(ajv.validate(recipeEnumSchema, recipe)).toBe(true); 169 | }); 170 | 171 | it("rejects other values", () => { 172 | // @ts-expect-error 173 | recipe = { not: "a recipe" }; 174 | expect(ajv.validate(recipeEnumSchema, recipe)).toBe(false); 175 | 176 | // @ts-expect-error 177 | recipe = ["not", "a", "recipe"]; 178 | expect(ajv.validate(recipeEnumSchema, recipe)).toBe(false); 179 | }); 180 | }); 181 | 182 | describe("Object enum", () => { 183 | const catEnumSchema = { 184 | type: "object", 185 | enum: [ 186 | { name: "Garfield", age: 13 }, 187 | { name: "Billy", age: 5 }, 188 | "not a cat", 189 | ], 190 | } as const; 191 | 192 | type Cat = FromSchema; 193 | let catInstance: Cat; 194 | 195 | it("accepts valid objects", () => { 196 | catInstance = { name: "Garfield", age: 13 }; 197 | expect(ajv.validate(catEnumSchema, catInstance)).toBe(true); 198 | 199 | catInstance = { name: "Billy", age: 5 }; 200 | expect(ajv.validate(catEnumSchema, catInstance)).toBe(true); 201 | }); 202 | 203 | it("rejects invalid object", () => { 204 | // @ts-expect-error 205 | catInstance = { name: "Billy", age: 6 }; 206 | expect(ajv.validate(catEnumSchema, catInstance)).toBe(false); 207 | }); 208 | 209 | it("rejects non-object values", () => { 210 | // @ts-expect-error 211 | catInstance = "not a cat"; 212 | expect(ajv.validate(catEnumSchema, catInstance)).toBe(false); 213 | 214 | // @ts-expect-error 215 | catInstance = 42; 216 | expect(ajv.validate(catEnumSchema, catInstance)).toBe(false); 217 | }); 218 | }); 219 | 220 | describe("TS enums", () => { 221 | enum Food { 222 | Pizza = "Pizza", 223 | Tacos = "Tacos", 224 | Fries = "Fries", 225 | } 226 | 227 | it("infers correct partial enum", () => { 228 | const pizzaTacosSchema = { 229 | enum: [Food.Pizza, Food.Tacos], 230 | } as const; 231 | 232 | type PizzaTacos = FromSchema; 233 | let pizzaOrTacos: PizzaTacos; 234 | 235 | pizzaOrTacos = Food.Pizza; 236 | expect(ajv.validate(pizzaTacosSchema, pizzaOrTacos)).toBe(true); 237 | 238 | pizzaOrTacos = Food.Tacos; 239 | expect(ajv.validate(pizzaTacosSchema, pizzaOrTacos)).toBe(true); 240 | 241 | // @ts-expect-error 242 | pizzaOrTacos = Food.Fries; 243 | expect(ajv.validate(pizzaTacosSchema, pizzaOrTacos)).toBe(false); 244 | }); 245 | 246 | it("reconstructs whole enum", () => { 247 | const foodSchema = { 248 | enum: Object.values(Food), 249 | }; 250 | 251 | type FoodType = FromSchema; 252 | let food: FoodType; 253 | 254 | food = Food.Pizza; 255 | expect(ajv.validate(foodSchema, food)).toBe(true); 256 | 257 | food = Food.Tacos; 258 | expect(ajv.validate(foodSchema, food)).toBe(true); 259 | 260 | food = Food.Fries; 261 | expect(ajv.validate(foodSchema, food)).toBe(true); 262 | 263 | // @ts-expect-error 264 | food = "Not a food"; 265 | expect(ajv.validate(foodSchema, food)).toBe(false); 266 | }); 267 | }); 268 | 269 | describe("TS enums (with type)", () => { 270 | enum Food { 271 | Pizza = "Pizza", 272 | Tacos = "Tacos", 273 | Fries = "Fries", 274 | } 275 | 276 | it("infers correct enum", () => { 277 | const foodSchema = { 278 | type: "string", 279 | enum: Object.values(Food), 280 | } as const; 281 | 282 | type FoodType = FromSchema; 283 | let food: FoodType; 284 | 285 | food = Food.Pizza; 286 | expect(ajv.validate(foodSchema, food)).toBe(true); 287 | 288 | food = Food.Tacos; 289 | expect(ajv.validate(foodSchema, food)).toBe(true); 290 | 291 | food = Food.Fries; 292 | expect(ajv.validate(foodSchema, food)).toBe(true); 293 | 294 | // @ts-expect-error 295 | food = "Not a food"; 296 | expect(ajv.validate(foodSchema, food)).toBe(false); 297 | }); 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /src/parse-schema/ifThenElse.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | import type { MergeSubSchema } from "./utils"; 7 | 8 | /** 9 | * JSON schemas of conditionally applied JSON schemas 10 | * @example 11 | * const conditionalSchema = { 12 | * type: "object", 13 | * if: { 14 | * properties: { 15 | * petKind: { 16 | * const: "dog" 17 | * } 18 | * } 19 | * }, 20 | * then: { 21 | * required: ["bark"] 22 | * }, 23 | * else: { 24 | * required: ["meow"] 25 | * } 26 | * } 27 | */ 28 | export type IfThenElseSchema = JSONSchema & { 29 | if: JSONSchema; 30 | then?: JSONSchema; 31 | else?: JSONSchema; 32 | }; 33 | 34 | /** 35 | * Recursively parses a conditionally applied JSON schema to a meta-type. 36 | * 37 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 38 | * @param IF_THEN_ELSE_SCHEMA JSONSchema (conditioned) 39 | * @param OPTIONS Parsing options 40 | * @returns Meta-type 41 | */ 42 | export type ParseIfThenElseSchema< 43 | IF_THEN_ELSE_SCHEMA extends IfThenElseSchema, 44 | OPTIONS extends ParseSchemaOptions, 45 | REST_SCHEMA extends JSONSchema = Omit< 46 | IF_THEN_ELSE_SCHEMA, 47 | "if" | "then" | "else" 48 | >, 49 | IF_SCHEMA extends JSONSchema = MergeSubSchema< 50 | REST_SCHEMA, 51 | IF_THEN_ELSE_SCHEMA["if"] 52 | >, 53 | PARSED_THEN_SCHEMA = IF_THEN_ELSE_SCHEMA extends { then: JSONSchema } 54 | ? M.$Intersect< 55 | ParseSchema, 56 | ParseSchema< 57 | MergeSubSchema, 58 | OPTIONS 59 | > 60 | > 61 | : ParseSchema, 62 | PARSED_ELSE_SCHEMA = IF_THEN_ELSE_SCHEMA extends { else: JSONSchema } 63 | ? M.$Intersect< 64 | M.$Exclude< 65 | ParseSchema, 66 | ParseSchema 67 | >, 68 | ParseSchema< 69 | MergeSubSchema, 70 | OPTIONS 71 | > 72 | > 73 | : M.$Exclude< 74 | ParseSchema, 75 | ParseSchema 76 | >, 77 | > = M.$Intersect< 78 | M.$Union, 79 | ParseSchema 80 | >; 81 | -------------------------------------------------------------------------------- /src/parse-schema/ifThenElse.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | import { ajv } from "./ajv.util.test"; 6 | 7 | describe("If/Then/Else schemas", () => { 8 | describe("if, then & else", () => { 9 | const petSchema = { 10 | type: "object", 11 | properties: { 12 | type: { enum: ["cat", "dog"] }, 13 | dogRace: { type: "string" }, 14 | catRace: { type: "string" }, 15 | }, 16 | required: ["type"], 17 | if: { properties: { type: { const: "dog" } } }, 18 | then: { 19 | required: ["dogRace"], 20 | }, 21 | else: { 22 | required: ["catRace"], 23 | }, 24 | } as const; 25 | 26 | type Pet = FromSchema; 27 | let petInstance: Pet; 28 | 29 | it("accepts valid dogs", () => { 30 | petInstance = { type: "dog", dogRace: "poodle" }; 31 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 32 | }); 33 | 34 | it("accepts valid cats", () => { 35 | petInstance = { type: "cat", catRace: "persan" }; 36 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 37 | }); 38 | 39 | it("rejects invalid dogs/cats", () => { 40 | // @ts-expect-error 41 | petInstance = { type: "dog" }; 42 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 43 | 44 | // @ts-expect-error 45 | petInstance = { type: "dog", catRace: "persan" }; 46 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 47 | 48 | // @ts-expect-error 49 | petInstance = { type: "cat" }; 50 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 51 | 52 | // @ts-expect-error 53 | petInstance = { type: "cat", dogRace: "poodle" }; 54 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 55 | 56 | // @ts-expect-error 57 | petInstance = { type: "duck" }; 58 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 59 | }); 60 | }); 61 | 62 | describe("only if & then", () => { 63 | const petSchema = { 64 | type: "object", 65 | properties: { 66 | type: { enum: ["cat", "dog"] }, 67 | dogRace: { type: "string" }, 68 | catRace: { type: "string" }, 69 | }, 70 | required: ["type"], 71 | if: { properties: { type: { const: "dog" } } }, 72 | then: { 73 | required: ["dogRace"], 74 | }, 75 | } as const; 76 | 77 | type Pet = FromSchema; 78 | let petInstance: Pet; 79 | 80 | it("accepts valid dogs", () => { 81 | petInstance = { type: "dog", dogRace: "poodle" }; 82 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 83 | }); 84 | 85 | it("accepts valid cats", () => { 86 | petInstance = { type: "cat" }; 87 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 88 | 89 | petInstance = { type: "cat", catRace: "persan" }; 90 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 91 | 92 | petInstance = { type: "cat", dogRace: "poodle" }; 93 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 94 | }); 95 | 96 | it("rejects invalid dogs/cats", () => { 97 | // @ts-expect-error 98 | petInstance = { type: "dog" }; 99 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 100 | 101 | // @ts-expect-error 102 | petInstance = { type: "dog", catRace: "persan" }; 103 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 104 | 105 | // @ts-expect-error 106 | petInstance = { type: "duck" }; 107 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 108 | }); 109 | }); 110 | 111 | describe("only if & else", () => { 112 | const petSchema = { 113 | type: "object", 114 | properties: { 115 | type: { enum: ["cat", "dog"] }, 116 | dogRace: { type: "string" }, 117 | catRace: { type: "string" }, 118 | }, 119 | required: ["type"], 120 | if: { properties: { type: { const: "dog" } } }, 121 | else: { 122 | required: ["catRace"], 123 | }, 124 | } as const; 125 | 126 | type Pet = FromSchema; 127 | let petInstance: Pet; 128 | 129 | it("accepts valid dogs", () => { 130 | petInstance = { type: "dog" }; 131 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 132 | 133 | petInstance = { type: "dog", dogRace: "poodle" }; 134 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 135 | 136 | petInstance = { type: "dog", catRace: "persan" }; 137 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 138 | }); 139 | 140 | it("accepts valid cats", () => { 141 | petInstance = { type: "cat", catRace: "persan" }; 142 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 143 | }); 144 | 145 | it("rejects invalid dogs/cats", () => { 146 | // @ts-expect-error 147 | petInstance = { type: "cat" }; 148 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 149 | 150 | // @ts-expect-error 151 | petInstance = { type: "cat", dogRace: "poodle" }; 152 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 153 | 154 | // @ts-expect-error 155 | petInstance = { type: "duck" }; 156 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 157 | }); 158 | }); 159 | 160 | describe("Closed to closed object (unevaluated properties)", () => { 161 | // Example from https://json-schema.org/understanding-json-schema/reference/object#unevaluatedproperties 162 | const addressSchema = { 163 | type: "object", 164 | properties: { 165 | street_address: { type: "string" }, 166 | city: { type: "string" }, 167 | state: { type: "string" }, 168 | type: { enum: ["residential", "business"] }, 169 | }, 170 | required: ["street_address", "city", "state", "type"], 171 | if: { 172 | type: "object", 173 | properties: { 174 | type: { const: "business" }, 175 | }, 176 | required: ["type"], 177 | }, 178 | then: { 179 | properties: { 180 | department: { type: "string" }, 181 | }, 182 | }, 183 | unevaluatedProperties: false, 184 | } as const; 185 | 186 | type Address = FromSchema< 187 | typeof addressSchema, 188 | { parseIfThenElseKeywords: true } 189 | >; 190 | let address: Address; 191 | 192 | type ExpectedAddress = 193 | | { 194 | street_address: string; 195 | city: string; 196 | state: string; 197 | type: "business"; 198 | department?: string | undefined; 199 | } 200 | | { 201 | street_address: string; 202 | city: string; 203 | state: string; 204 | type: "residential"; 205 | }; 206 | type AssertAddress = A.Equals; 207 | const assertAddress: AssertAddress = 1; 208 | assertAddress; 209 | 210 | it("accepts valid objects", () => { 211 | address = { 212 | street_address: "1600 Pennsylvania Avenue NW", 213 | city: "Washington", 214 | state: "DC", 215 | type: "business", 216 | department: "HR", 217 | }; 218 | expect(ajv.validate(addressSchema, address)).toBe(true); 219 | }); 220 | 221 | it("rejects unevaluated properties", () => { 222 | address = { 223 | street_address: "1600 Pennsylvania Avenue NW", 224 | city: "Washington", 225 | state: "DC", 226 | type: "residential", 227 | // @ts-expect-error 228 | department: "HR", 229 | }; 230 | expect(ajv.validate(addressSchema, address)).toBe(false); 231 | }); 232 | }); 233 | 234 | describe("additional items (incorrect)", () => { 235 | const petSchema = { 236 | type: "array", 237 | items: [ 238 | { enum: ["cat", "dog"] }, 239 | { enum: ["poodle", "beagle", "husky"] }, 240 | ], 241 | if: { 242 | items: [{ const: "dog" }], 243 | }, 244 | then: { minItems: 2, additionalItems: false }, 245 | else: { maxItems: 1 }, 246 | } as const; 247 | 248 | type Pet = FromSchema; 249 | let petInstance: Pet; 250 | 251 | it("rejects invalid dog instances", () => { 252 | // @ts-expect-error 253 | petInstance = ["dog"]; 254 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 255 | 256 | // accepts additionalItems as additionalItems is not bound to items 257 | petInstance = ["dog", "poodle", "other"]; 258 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 259 | }); 260 | }); 261 | 262 | describe("additional items (correct)", () => { 263 | const petSchema = { 264 | type: "array", 265 | items: [ 266 | { enum: ["cat", "dog"] }, 267 | { enum: ["poodle", "beagle", "husky"] }, 268 | ], 269 | if: { 270 | items: [{ const: "dog" }], 271 | }, 272 | then: { items: [{ const: "dog" }], additionalItems: false }, 273 | } as const; 274 | 275 | type Pet = FromSchema; 276 | let petInstance: Pet; 277 | 278 | it("rejects invalid dog instances", () => { 279 | petInstance = ["dog"]; 280 | expect(ajv.validate(petSchema, petInstance)).toBe(true); 281 | 282 | // @ts-expect-error 283 | petInstance = ["dog", "poodle", "other"]; 284 | expect(ajv.validate(petSchema, petInstance)).toBe(false); 285 | }); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /src/parse-schema/index.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { DeserializationPattern, JSONSchema } from "~/definitions"; 4 | import type { And, DoesExtend } from "~/type-utils"; 5 | 6 | import type { AllOfSchema, ParseAllOfSchema } from "./allOf"; 7 | import type { AnyOfSchema, ParseAnyOfSchema } from "./anyOf"; 8 | import type { ConstSchema, ParseConstSchema } from "./const"; 9 | import type { DeserializeSchema } from "./deserialize"; 10 | import type { EnumSchema, ParseEnumSchema } from "./enum"; 11 | import type { IfThenElseSchema, ParseIfThenElseSchema } from "./ifThenElse"; 12 | import type { 13 | MultipleTypesSchema, 14 | ParseMultipleTypesSchema, 15 | } from "./multipleTypes"; 16 | import type { NotSchema, ParseNotSchema } from "./not"; 17 | import type { NullableSchema, ParseNullableSchema } from "./nullable"; 18 | import type { OneOfSchema, ParseOneOfSchema } from "./oneOf"; 19 | import type { ParseReferenceSchema, ReferencingSchema } from "./references"; 20 | import type { ParseSingleTypeSchema, SingleTypeSchema } from "./singleType"; 21 | 22 | /** 23 | * Type constraint for the ParseSchema options 24 | */ 25 | export type ParseSchemaOptions = { 26 | /** 27 | * Wether to parse negated schemas or not (false by default) 28 | */ 29 | parseNotKeyword: boolean; 30 | /** 31 | * Wether to parse ifThenElse schemas or not (false by default) 32 | */ 33 | parseIfThenElseKeywords: boolean; 34 | /** 35 | * Wether to keep object defaulted properties optional or not (false by default) 36 | */ 37 | keepDefaultedPropertiesOptional: boolean; 38 | /** 39 | * The initial schema provided to `ParseSchema` 40 | */ 41 | rootSchema: JSONSchema; 42 | /** 43 | * To refer external schemas by ids 44 | */ 45 | references: Record; 46 | /** 47 | * To override inferred types if some pattern is matched 48 | */ 49 | deserialize: DeserializationPattern[] | false; 50 | }; 51 | 52 | /** 53 | * Recursively parses a JSON schema to a meta-type. Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 54 | * @param SCHEMA JSON schema 55 | * @param OPTIONS Parsing options 56 | * @returns Meta-type 57 | */ 58 | export type ParseSchema< 59 | SCHEMA extends JSONSchema, 60 | OPTIONS extends ParseSchemaOptions, 61 | RESULT = JSONSchema extends SCHEMA 62 | ? M.Any 63 | : SCHEMA extends true | string 64 | ? M.Any 65 | : SCHEMA extends false 66 | ? M.Never 67 | : SCHEMA extends NullableSchema 68 | ? ParseNullableSchema 69 | : SCHEMA extends ReferencingSchema 70 | ? ParseReferenceSchema 71 | : And< 72 | DoesExtend, 73 | DoesExtend 74 | > extends true 75 | ? SCHEMA extends IfThenElseSchema 76 | ? ParseIfThenElseSchema 77 | : never 78 | : And< 79 | DoesExtend, 80 | DoesExtend 81 | > extends true 82 | ? SCHEMA extends NotSchema 83 | ? ParseNotSchema 84 | : never 85 | : SCHEMA extends AllOfSchema 86 | ? ParseAllOfSchema 87 | : SCHEMA extends OneOfSchema 88 | ? ParseOneOfSchema 89 | : SCHEMA extends AnyOfSchema 90 | ? ParseAnyOfSchema 91 | : SCHEMA extends EnumSchema 92 | ? ParseEnumSchema 93 | : SCHEMA extends ConstSchema 94 | ? ParseConstSchema 95 | : SCHEMA extends MultipleTypesSchema 96 | ? ParseMultipleTypesSchema 97 | : SCHEMA extends SingleTypeSchema 98 | ? ParseSingleTypeSchema 99 | : M.Any, 100 | > = OPTIONS extends { deserialize: DeserializationPattern[] } 101 | ? M.$Intersect, RESULT> 102 | : RESULT; 103 | -------------------------------------------------------------------------------- /src/parse-schema/multipleTypes.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema, JSONSchemaType } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | 7 | /** 8 | * JSON schemas of type unions 9 | * @example 10 | * const typeUnionSchema = { 11 | * type: ["number", "string"] 12 | * } 13 | */ 14 | export type MultipleTypesSchema = JSONSchema & 15 | Readonly<{ type: readonly JSONSchemaType[] }>; 16 | 17 | /** 18 | * Recursively parses a multiple type JSON schema to a meta-type. 19 | * 20 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 21 | * @param MULTI_TYPE_SCHEMA JSONSchema (single type) 22 | * @param OPTIONS Parsing options 23 | * @returns Meta-type 24 | */ 25 | export type ParseMultipleTypesSchema< 26 | MULTI_TYPE_SCHEMA extends MultipleTypesSchema, 27 | OPTIONS extends ParseSchemaOptions, 28 | > = M.$Union< 29 | RecurseOnMixedSchema 30 | >; 31 | 32 | /** 33 | * Recursively parses a multiple type JSON schema to the union of its types (merged with root schema). 34 | * @param TYPES JSONSchemaType[] 35 | * @param ROOT_MULTI_TYPE_SCHEMA Root JSONSchema (multiple types) 36 | * @param OPTIONS Parsing options 37 | * @returns Meta-type 38 | */ 39 | type RecurseOnMixedSchema< 40 | TYPES extends readonly JSONSchemaType[], 41 | ROOT_MULTI_TYPE_SCHEMA extends MultipleTypesSchema, 42 | OPTIONS extends ParseSchemaOptions, 43 | RESULT = never, 44 | > = TYPES extends readonly [infer TYPES_HEAD, ...infer TYPES_TAIL] 45 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 46 | TYPES_HEAD extends JSONSchemaType 47 | ? TYPES_TAIL extends readonly JSONSchemaType[] 48 | ? RecurseOnMixedSchema< 49 | TYPES_TAIL, 50 | ROOT_MULTI_TYPE_SCHEMA, 51 | OPTIONS, 52 | | RESULT 53 | | ParseSchema< 54 | Omit & { type: TYPES_HEAD }, 55 | OPTIONS 56 | > 57 | > 58 | : never 59 | : never 60 | : RESULT; 61 | -------------------------------------------------------------------------------- /src/parse-schema/multipleTypes.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Mixed types schemas", () => { 6 | describe("Primitives", () => { 7 | const simpleTypesSchema = { 8 | type: ["null", "boolean", "integer"], 9 | } as const; 10 | 11 | type Simple = FromSchema; 12 | let simpleInstance: Simple; 13 | 14 | it("accepts null value", () => { 15 | simpleInstance = null; 16 | expect(ajv.validate(simpleTypesSchema, simpleInstance)).toBe(true); 17 | }); 18 | 19 | it("accepts boolean value", () => { 20 | simpleInstance = true; 21 | expect(ajv.validate(simpleTypesSchema, simpleInstance)).toBe(true); 22 | }); 23 | 24 | it("accepts number value", () => { 25 | simpleInstance = 42; 26 | expect(ajv.validate(simpleTypesSchema, simpleInstance)).toBe(true); 27 | }); 28 | 29 | it("rejects string value", () => { 30 | // @ts-expect-error 31 | simpleInstance = "string"; 32 | expect(ajv.validate(simpleTypesSchema, simpleInstance)).toBe(false); 33 | }); 34 | 35 | it("rejects array value", () => { 36 | // @ts-expect-error 37 | simpleInstance = [null, true, 3]; 38 | expect(ajv.validate(simpleTypesSchema, simpleInstance)).toBe(false); 39 | }); 40 | }); 41 | 42 | describe("Number or array", () => { 43 | const complexTypesSchema = { 44 | type: ["number", "array"], 45 | items: { type: "string" }, 46 | } as const; 47 | 48 | type Complex = FromSchema; 49 | let complexInstance: Complex; 50 | 51 | it("accepts number value", () => { 52 | complexInstance = 42; 53 | expect(ajv.validate(complexTypesSchema, complexInstance)).toBe(true); 54 | }); 55 | 56 | it("accepts string array value", () => { 57 | complexInstance = ["apples", "tomatoes"]; 58 | expect(ajv.validate(complexTypesSchema, complexInstance)).toBe(true); 59 | }); 60 | 61 | it("rejects string or number array value", () => { 62 | // @ts-expect-error 63 | complexInstance = ["apples", 42]; 64 | expect(ajv.validate(complexTypesSchema, complexInstance)).toBe(false); 65 | }); 66 | 67 | it("rejects other value", () => { 68 | // @ts-expect-error 69 | complexInstance = { not: "a number", neither: ["a", "string", "array"] }; 70 | expect(ajv.validate(complexTypesSchema, complexInstance)).toBe(false); 71 | }); 72 | }); 73 | 74 | describe("Tuple or object", () => { 75 | const uberComplexTypesSchema = { 76 | type: ["array", "object"], 77 | items: [ 78 | { type: "number" }, 79 | { type: "string" }, 80 | { 81 | type: "object", 82 | properties: { descr: { type: "string" } }, 83 | required: ["descr"], 84 | }, 85 | ], 86 | additionalItems: false, 87 | properties: { name: { type: "string" }, description: { type: "string" } }, 88 | required: ["name"], 89 | } as const; 90 | 91 | type UberComplex = FromSchema; 92 | let uberComplexInstance: UberComplex; 93 | 94 | it("accepts object with required & valid properties", () => { 95 | uberComplexInstance = { name: "Garfield" }; 96 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 97 | true, 98 | ); 99 | 100 | uberComplexInstance = { name: "Garfield", description: "a cool cat" }; 101 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 102 | true, 103 | ); 104 | }); 105 | 106 | it("rejects object with invalid property", () => { 107 | // @ts-expect-error 108 | uberComplexInstance = { name: "Garfield", description: 42 }; 109 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 110 | false, 111 | ); 112 | }); 113 | 114 | it("accepts tuples with valid values", () => { 115 | uberComplexInstance = []; 116 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 117 | true, 118 | ); 119 | 120 | uberComplexInstance = [42]; 121 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 122 | true, 123 | ); 124 | 125 | uberComplexInstance = [42, "foo"]; 126 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 127 | true, 128 | ); 129 | 130 | uberComplexInstance = [42, "foo", { descr: "bar" }]; 131 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 132 | true, 133 | ); 134 | }); 135 | 136 | it("rejects tuple with invalid value", () => { 137 | // @ts-expect-error 138 | uberComplexInstance = ["42", "foo", { descr: "bar" }]; 139 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 140 | false, 141 | ); 142 | }); 143 | 144 | it("rejects tuple with additional items", () => { 145 | // @ts-expect-error 146 | uberComplexInstance = [42, "foo", { descr: "bar" }, "baz"]; 147 | expect(ajv.validate(uberComplexTypesSchema, uberComplexInstance)).toBe( 148 | false, 149 | ); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/parse-schema/noSchema.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema } from "~/definitions"; 2 | import type { FromSchema } from "~/index"; 3 | 4 | import { ajv } from "./ajv.util.test"; 5 | 6 | describe("No schema", () => { 7 | describe("Empty", () => { 8 | const emptySchema = {} as JSONSchema; 9 | type Any = FromSchema; 10 | let anyInstance: Any; 11 | 12 | it("accepts any value", () => { 13 | anyInstance = null; 14 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 15 | 16 | anyInstance = true; 17 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 18 | 19 | anyInstance = "string"; 20 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 21 | 22 | anyInstance = 42; 23 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 24 | 25 | anyInstance = { foo: "bar" }; 26 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 27 | 28 | anyInstance = ["foo", "bar"]; 29 | expect(ajv.validate(emptySchema, anyInstance)).toBe(true); 30 | }); 31 | }); 32 | 33 | describe("True", () => { 34 | type Unknown = FromSchema; 35 | let anyInstance: Unknown; 36 | 37 | it("accepts any value", () => { 38 | anyInstance = null; 39 | expect(ajv.validate(true, anyInstance)).toBe(true); 40 | 41 | anyInstance = true; 42 | expect(ajv.validate(true, anyInstance)).toBe(true); 43 | 44 | anyInstance = "string"; 45 | expect(ajv.validate(true, anyInstance)).toBe(true); 46 | 47 | anyInstance = 42; 48 | expect(ajv.validate(true, anyInstance)).toBe(true); 49 | 50 | anyInstance = { foo: "bar" }; 51 | expect(ajv.validate(true, anyInstance)).toBe(true); 52 | 53 | anyInstance = ["foo", "bar"]; 54 | expect(ajv.validate(true, anyInstance)).toBe(true); 55 | }); 56 | }); 57 | 58 | describe("False", () => { 59 | type Never = FromSchema; 60 | let neverInstance: Never; 61 | 62 | it("rejects any value", () => { 63 | // @ts-expect-error 64 | neverInstance = null; 65 | expect(ajv.validate(false, neverInstance)).toBe(false); 66 | 67 | // @ts-expect-error 68 | neverInstance = true; 69 | expect(ajv.validate(false, neverInstance)).toBe(false); 70 | 71 | // @ts-expect-error 72 | neverInstance = "string"; 73 | expect(ajv.validate(false, neverInstance)).toBe(false); 74 | 75 | // @ts-expect-error 76 | neverInstance = 42; 77 | expect(ajv.validate(false, neverInstance)).toBe(false); 78 | 79 | // @ts-expect-error 80 | neverInstance = { foo: "bar" }; 81 | expect(ajv.validate(false, neverInstance)).toBe(false); 82 | 83 | // @ts-expect-error 84 | neverInstance = ["foo", "bar"]; 85 | expect(ajv.validate(false, neverInstance)).toBe(false); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/parse-schema/not.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | import type { MergeSubSchema } from "./utils"; 7 | 8 | /** 9 | * JSON schemas of JSON schema exclusions 10 | * @example 11 | * const exclusionSchema = { 12 | * type: "string", 13 | * not: { 14 | * enum: ["Bummer", "Silly", "Lazy sod !"] 15 | * } 16 | * } 17 | */ 18 | export type NotSchema = JSONSchema & Readonly<{ not: JSONSchema }>; 19 | 20 | /** 21 | * Any possible meta-type 22 | */ 23 | type AllTypes = M.Union< 24 | | M.Primitive 25 | | M.Primitive 26 | | M.Primitive 27 | | M.Primitive 28 | | M.Array 29 | | M.Object<{}, never, M.Any> 30 | >; 31 | 32 | /** 33 | * Recursively parses a JSON schema exclusion to a meta-type. 34 | * 35 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 36 | * @param NOT_SCHEMA JSONSchema (exclusion) 37 | * @param OPTIONS Parsing options 38 | * @returns Meta-type 39 | */ 40 | export type ParseNotSchema< 41 | NOT_SCHEMA extends NotSchema, 42 | OPTIONS extends ParseSchemaOptions, 43 | PARSED_REST_SCHEMA = ParseSchema, OPTIONS>, 44 | EXCLUSION = M.$Exclude< 45 | PARSED_REST_SCHEMA extends M.AnyType 46 | ? M.$Intersect 47 | : PARSED_REST_SCHEMA, 48 | ParseSchema< 49 | MergeSubSchema, NOT_SCHEMA["not"]>, 50 | OPTIONS 51 | > 52 | >, 53 | > = EXCLUSION extends M.Never ? PARSED_REST_SCHEMA : EXCLUSION; 54 | -------------------------------------------------------------------------------- /src/parse-schema/not.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Not schemas", () => { 6 | describe("All but boolean", () => { 7 | const notBoolSchema = { 8 | not: { type: "boolean" }, 9 | } as const; 10 | 11 | // @ts-ignore This type can raise an error (in VS Code only) 12 | type NotBool = FromSchema; 13 | let notBoolInstance: NotBool; 14 | 15 | it("rejects boolean", () => { 16 | // @ts-expect-error 17 | notBoolInstance = false; 18 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(false); 19 | 20 | // @ts-expect-error 21 | notBoolInstance = true; 22 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(false); 23 | }); 24 | 25 | it("accepts any other value", () => { 26 | notBoolInstance = null; 27 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(true); 28 | 29 | notBoolInstance = 42; 30 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(true); 31 | 32 | notBoolInstance = "string"; 33 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(true); 34 | 35 | notBoolInstance = { any: "object" }; 36 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(true); 37 | 38 | notBoolInstance = ["any", "object"]; 39 | expect(ajv.validate(notBoolSchema, notBoolInstance)).toBe(true); 40 | }); 41 | }); 42 | 43 | describe("Tuple of length 1 or 3", () => { 44 | const tupleSchema = { 45 | type: "array", 46 | items: [{ const: 1 }, { const: 2 }, { const: 3 }], 47 | minItems: 1, 48 | not: { const: [1, 2] }, 49 | } as const; 50 | 51 | type Tuple = FromSchema; 52 | let tuple: Tuple; 53 | 54 | it("rejects tuple of incorrect length", () => { 55 | // @ts-expect-error 56 | tuple = []; 57 | expect(ajv.validate(tupleSchema, tuple)).toBe(false); 58 | 59 | // @ts-expect-error 60 | tuple = [1, 2]; 61 | expect(ajv.validate(tupleSchema, tuple)).toBe(false); 62 | }); 63 | 64 | it("accepts tuple of correct lengths", () => { 65 | tuple = [1]; 66 | expect(ajv.validate(tupleSchema, tuple)).toBe(true); 67 | 68 | tuple = [1, 2, 3]; 69 | expect(ajv.validate(tupleSchema, tuple)).toBe(true); 70 | }); 71 | }); 72 | 73 | describe("Tuple of length 3", () => { 74 | const tupleSchema = { 75 | type: "array", 76 | items: [{ const: 1 }, { const: 2 }, { const: 3 }], 77 | not: { maxItems: 2 }, 78 | } as const; 79 | 80 | type Tuple = FromSchema; 81 | let tuple: Tuple; 82 | 83 | it("rejects tuple of incorrect length", () => { 84 | // @ts-expect-error 85 | tuple = []; 86 | expect(ajv.validate(tupleSchema, tuple)).toBe(false); 87 | 88 | // @ts-expect-error 89 | tuple = [1]; 90 | expect(ajv.validate(tupleSchema, tuple)).toBe(false); 91 | 92 | // @ts-expect-error 93 | tuple = [1, 2]; 94 | expect(ajv.validate(tupleSchema, tuple)).toBe(false); 95 | }); 96 | 97 | it("accepts tuple of correct lengths", () => { 98 | tuple = [1, 2, 3]; 99 | expect(ajv.validate(tupleSchema, tuple)).toBe(true); 100 | }); 101 | }); 102 | 103 | describe("Enum not const", () => { 104 | const correctLanguageSchema = { 105 | type: "string", 106 | enum: ["genious", "regular", "idiot"], 107 | not: { const: "idiot" }, 108 | } as const; 109 | 110 | type CorrectLanguage = FromSchema< 111 | typeof correctLanguageSchema, 112 | { parseNotKeyword: true } 113 | >; 114 | let correctLanguage: CorrectLanguage; 115 | 116 | it("rejects incorrect language", () => { 117 | // @ts-expect-error 118 | correctLanguage = "idiot"; 119 | expect(ajv.validate(correctLanguageSchema, correctLanguage)).toBe(false); 120 | }); 121 | 122 | it("accepts correct language", () => { 123 | correctLanguage = "genious"; 124 | expect(ajv.validate(correctLanguageSchema, correctLanguage)).toBe(true); 125 | 126 | correctLanguage = "regular"; 127 | expect(ajv.validate(correctLanguageSchema, correctLanguage)).toBe(true); 128 | }); 129 | }); 130 | 131 | describe("additionalItems", () => { 132 | const openArraySchema1 = { 133 | type: "array", 134 | items: [{ const: 0 }, { enum: [0, 1] }], 135 | not: { const: [0, 0], additionalItems: false }, 136 | } as const; 137 | 138 | type OpenArray1 = FromSchema< 139 | typeof openArraySchema1, 140 | { parseNotKeyword: true } 141 | >; 142 | let openArray1: OpenArray1; 143 | 144 | it("accepts correct item", () => { 145 | // Still works as additionalItems is not bound to items 146 | openArray1 = [0, 0, 1]; 147 | expect(ajv.validate(openArraySchema1, openArray1)).toBe(true); 148 | }); 149 | 150 | const openArraySchema2 = { 151 | type: "array", 152 | items: [{ const: 0 }, { const: 1 }], 153 | not: { items: [{ const: 0 }, { const: 1 }], additionalItems: false }, 154 | } as const; 155 | 156 | type OpenArray2 = FromSchema< 157 | typeof openArraySchema2, 158 | { parseNotKeyword: true } 159 | >; 160 | let openArray2: OpenArray2; 161 | 162 | it("accepts correct item", () => { 163 | openArray2 = [0, 1, 2]; 164 | expect(ajv.validate(openArraySchema2, openArray2)).toBe(true); 165 | 166 | // Is correctly rejected but impossible to throw right now as [] can be assigned to ...unknown[] 167 | openArray2 = [0, 1]; 168 | expect(ajv.validate(openArraySchema2, openArray2)).toBe(false); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/parse-schema/nullable.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | 7 | /** 8 | * JSON schemas of any type or `null` 9 | * @example 10 | * const nullableSchema = { 11 | * type: "string", 12 | * nullable: true 13 | * } 14 | */ 15 | export type NullableSchema = JSONSchema & Readonly<{ nullable: boolean }>; 16 | 17 | /** 18 | * Parses a nullable JSON schema to a meta-type. 19 | * 20 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 21 | * @param NULLABLE_SCHEMA JSONSchema (nullable) 22 | * @param OPTIONS Parsing options 23 | * @returns Meta-type 24 | */ 25 | export type ParseNullableSchema< 26 | NULLABLE_SCHEMA extends NullableSchema, 27 | OPTIONS extends ParseSchemaOptions, 28 | PARSED_REST_SCHEMA = ParseSchema, OPTIONS>, 29 | > = NULLABLE_SCHEMA extends Readonly<{ nullable: true }> 30 | ? M.$Union | PARSED_REST_SCHEMA> 31 | : PARSED_REST_SCHEMA; 32 | -------------------------------------------------------------------------------- /src/parse-schema/nullable.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Nullable schemas", () => { 6 | describe("Simple nullable schema", () => { 7 | const nullableBooleanSchema = { type: "boolean", nullable: true } as const; 8 | 9 | type NullableBoolean = FromSchema; 10 | let nullableBooleanInst: NullableBoolean; 11 | 12 | it("accepts null value", () => { 13 | nullableBooleanInst = true; 14 | expect(ajv.validate(nullableBooleanSchema, nullableBooleanInst)).toBe( 15 | true, 16 | ); 17 | 18 | nullableBooleanInst = null; 19 | expect(ajv.validate(nullableBooleanSchema, nullableBooleanInst)).toBe( 20 | true, 21 | ); 22 | }); 23 | }); 24 | 25 | describe("Simple non nullable schema", () => { 26 | const booleanSchema = { type: "boolean" } as const; 27 | const nonNullableBooleanSchema = { 28 | type: "boolean", 29 | nullable: false, 30 | } as const; 31 | 32 | type Boolean = FromSchema; 33 | let booleanInst: Boolean; 34 | 35 | type NonNullableBoolean = FromSchema; 36 | let nonNullableBooleanInst: NonNullableBoolean; 37 | 38 | it("rejects null value", () => { 39 | // @ts-expect-error 40 | booleanInst = null; 41 | expect(ajv.validate(booleanSchema, booleanInst)).toBe(false); 42 | 43 | // @ts-expect-error 44 | nonNullableBooleanInst = null; 45 | expect( 46 | ajv.validate(nonNullableBooleanSchema, nonNullableBooleanInst), 47 | ).toBe(false); 48 | }); 49 | }); 50 | 51 | describe("Nested nullable schema", () => { 52 | const objectWithNullablePropSchema = { 53 | type: "object", 54 | properties: { 55 | nullable: { type: "string", nullable: true }, 56 | explicitNonNullable: { type: "string", nullable: false }, 57 | implicitNonNullable: { type: "string" }, 58 | }, 59 | required: ["nullable", "explicitNonNullable", "implicitNonNullable"], 60 | additionalProperties: false, 61 | } as const; 62 | 63 | type ObjectWithNullableProp = FromSchema< 64 | typeof objectWithNullablePropSchema 65 | >; 66 | let objectInst: ObjectWithNullableProp; 67 | 68 | it("accepts null on nullable property", () => { 69 | objectInst = { 70 | nullable: "str", 71 | explicitNonNullable: "str", 72 | implicitNonNullable: "str", 73 | }; 74 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe(true); 75 | 76 | objectInst = { 77 | nullable: null, 78 | explicitNonNullable: "str", 79 | implicitNonNullable: "str", 80 | }; 81 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe(true); 82 | }); 83 | 84 | it("rejects null on non-nullable property", () => { 85 | objectInst = { 86 | nullable: "str", 87 | // @ts-expect-error 88 | explicitNonNullable: null, 89 | implicitNonNullable: "str", 90 | }; 91 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe( 92 | false, 93 | ); 94 | 95 | objectInst = { 96 | nullable: null, 97 | explicitNonNullable: "str", 98 | // @ts-expect-error 99 | implicitNonNullable: null, 100 | }; 101 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe( 102 | false, 103 | ); 104 | }); 105 | }); 106 | 107 | describe("Deeply nested nullable schema", () => { 108 | const objectWithNullablePropSchema = { 109 | type: "object", 110 | properties: { 111 | // TOIMPROVE: Make it work with booleans (for the moment, M.Exclude, M.Const> = M.Primitive => Could be M.Const) 112 | preventNullable: { type: "string", enum: ["true", "false"] }, 113 | potentiallyNullable: { 114 | type: "string", 115 | nullable: true, 116 | }, 117 | }, 118 | required: ["preventNullable", "potentiallyNullable"], 119 | additionalProperties: false, 120 | allOf: [ 121 | { 122 | if: { 123 | properties: { 124 | preventNullable: { const: "true" }, 125 | }, 126 | }, 127 | then: { 128 | properties: { 129 | potentiallyNullable: { 130 | type: "string", 131 | nullable: false, 132 | }, 133 | }, 134 | }, 135 | }, 136 | ], 137 | } as const; 138 | 139 | type ObjectWithNullableProp = FromSchema< 140 | typeof objectWithNullablePropSchema, 141 | { parseIfThenElseKeywords: true } 142 | >; 143 | let objectInst: ObjectWithNullableProp; 144 | 145 | it("accepts null on nullable property", () => { 146 | objectInst = { 147 | preventNullable: "false", 148 | potentiallyNullable: null, 149 | }; 150 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe(true); 151 | }); 152 | 153 | it("rejects null on non-nullable property", () => { 154 | // @ts-expect-error 155 | objectInst = { 156 | preventNullable: "true", 157 | potentiallyNullable: null, 158 | }; 159 | expect(ajv.validate(objectWithNullablePropSchema, objectInst)).toBe( 160 | false, 161 | ); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /src/parse-schema/object.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | 7 | /** 8 | * JSON schemas of objects 9 | * @example 10 | * const objectSchema = { 11 | * type: "object", 12 | * properties: { 13 | * color: { 14 | * type: "string" 15 | * } 16 | * } 17 | * } 18 | */ 19 | export type ObjectSchema = JSONSchema & Readonly<{ type: "object" }>; 20 | 21 | /** 22 | * Parses an object JSON schema to a meta-type. 23 | * 24 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 25 | * @param OBJECT_SCHEMA JSONSchema (object type) 26 | * @param OPTIONS Parsing options 27 | * @returns Meta-type 28 | */ 29 | export type ParseObjectSchema< 30 | OBJECT_SCHEMA extends ObjectSchema, 31 | OPTIONS extends ParseSchemaOptions, 32 | > = OBJECT_SCHEMA extends Readonly<{ 33 | properties: Readonly>; 34 | }> 35 | ? M.$Object< 36 | { 37 | [KEY in keyof OBJECT_SCHEMA["properties"]]: ParseSchema< 38 | OBJECT_SCHEMA["properties"][KEY], 39 | OPTIONS 40 | >; 41 | }, 42 | GetRequired, 43 | GetOpenProps, 44 | GetClosedOnResolve 45 | > 46 | : M.$Object< 47 | {}, 48 | GetRequired, 49 | GetOpenProps, 50 | GetClosedOnResolve 51 | >; 52 | 53 | /** 54 | * Extracts the required keys of an object JSON schema 55 | * @param OBJECT_SCHEMA JSONSchema (object type) 56 | * @returns String 57 | */ 58 | type GetRequired< 59 | OBJECT_SCHEMA extends ObjectSchema, 60 | OPTIONS extends ParseSchemaOptions, 61 | > = 62 | | (OBJECT_SCHEMA extends Readonly<{ required: ReadonlyArray }> 63 | ? OBJECT_SCHEMA["required"][number] 64 | : never) 65 | | (OPTIONS["keepDefaultedPropertiesOptional"] extends true 66 | ? never 67 | : OBJECT_SCHEMA extends Readonly<{ 68 | properties: Readonly>; 69 | }> 70 | ? { 71 | [KEY in keyof OBJECT_SCHEMA["properties"] & 72 | string]: OBJECT_SCHEMA["properties"][KEY] extends Readonly<{ 73 | default: unknown; 74 | }> 75 | ? KEY 76 | : never; 77 | }[keyof OBJECT_SCHEMA["properties"] & string] 78 | : never); 79 | 80 | /** 81 | * Extracts and parses the additional and pattern properties (if any exists) of an object JSON schema 82 | * @param OBJECT_SCHEMA JSONSchema (object type) 83 | * @param OPTIONS Parsing options 84 | * @returns String 85 | */ 86 | type GetOpenProps< 87 | OBJECT_SCHEMA extends ObjectSchema, 88 | OPTIONS extends ParseSchemaOptions, 89 | > = OBJECT_SCHEMA extends Readonly<{ additionalProperties: JSONSchema }> 90 | ? OBJECT_SCHEMA extends Readonly<{ 91 | patternProperties: Record; 92 | }> 93 | ? AdditionalAndPatternProps< 94 | OBJECT_SCHEMA["additionalProperties"], 95 | OBJECT_SCHEMA["patternProperties"], 96 | OPTIONS 97 | > 98 | : ParseSchema 99 | : OBJECT_SCHEMA extends Readonly<{ 100 | patternProperties: Record; 101 | }> 102 | ? PatternProps 103 | : M.Any; 104 | 105 | /** 106 | * Extracts and parses the unevaluated properties (if any exists) of an object JSON schema 107 | * @param OBJECT_SCHEMA JSONSchema (object type) 108 | * @param OPTIONS Parsing options 109 | * @returns String 110 | */ 111 | type GetClosedOnResolve = 112 | OBJECT_SCHEMA extends Readonly<{ unevaluatedProperties: false }> 113 | ? true 114 | : false; 115 | 116 | /** 117 | * Extracts and parses the pattern properties of an object JSON schema 118 | * @param PATTERN_PROPERTY_SCHEMAS Record 119 | * @param OPTIONS Parsing options 120 | * @returns String 121 | */ 122 | type PatternProps< 123 | PATTERN_PROPERTY_SCHEMAS extends Readonly>, 124 | OPTIONS extends ParseSchemaOptions, 125 | > = M.$Union< 126 | { 127 | [KEY in keyof PATTERN_PROPERTY_SCHEMAS]: ParseSchema< 128 | PATTERN_PROPERTY_SCHEMAS[KEY], 129 | OPTIONS 130 | >; 131 | }[keyof PATTERN_PROPERTY_SCHEMAS] 132 | >; 133 | 134 | /** 135 | * Extracts, parses and unify the additional and pattern properties of an object JSON schema into a single meta-type 136 | * @param ADDITIONAL_PROPERTIES_SCHEMA JSONSchema 137 | * @param PATTERN_PROPERTY_SCHEMAS Record 138 | * @param OPTIONS Parsing options 139 | * @returns String 140 | */ 141 | type AdditionalAndPatternProps< 142 | ADDITIONAL_PROPERTIES_SCHEMA extends JSONSchema, 143 | PATTERN_PROPERTY_SCHEMAS extends Readonly>, 144 | OPTIONS extends ParseSchemaOptions, 145 | > = ADDITIONAL_PROPERTIES_SCHEMA extends boolean 146 | ? PatternProps 147 | : M.$Union< 148 | | ParseSchema 149 | | { 150 | [KEY in keyof PATTERN_PROPERTY_SCHEMAS]: ParseSchema< 151 | PATTERN_PROPERTY_SCHEMAS[KEY], 152 | OPTIONS 153 | >; 154 | }[keyof PATTERN_PROPERTY_SCHEMAS] 155 | >; 156 | -------------------------------------------------------------------------------- /src/parse-schema/oneOf.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | 5 | import type { ParseSchema, ParseSchemaOptions } from "./index"; 6 | import type { MergeSubSchema } from "./utils"; 7 | 8 | /** 9 | * JSON schemas of exclusive JSON schema unions 10 | * @example 11 | * const exclusiveUnionSchema = { 12 | * oneOf: [ 13 | * { type: "number" }, 14 | * { enum: [1, 2, "foo"] } // => 1 & 2 are not valid 15 | * ] 16 | * } 17 | */ 18 | export type OneOfSchema = JSONSchema & 19 | Readonly<{ oneOf: readonly JSONSchema[] }>; 20 | 21 | /** 22 | * Recursively parses an exclusive JSON schema union to a meta-type. 23 | * 24 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 25 | * @param ONE_OF_SCHEMA JSONSchema (exclusive schema union) 26 | * @param OPTIONS Parsing options 27 | * @returns Meta-type 28 | */ 29 | export type ParseOneOfSchema< 30 | ONE_OF_SCHEMA extends OneOfSchema, 31 | OPTIONS extends ParseSchemaOptions, 32 | > = M.$Union< 33 | RecurseOnOneOfSchema 34 | >; 35 | 36 | /** 37 | * Recursively parses a tuple of JSON schemas to the union of its parsed meta-types (merged with root schema). 38 | * @param SUB_SCHEMAS JSONSchema[] 39 | * @param ROOT_ONE_OF_SCHEMA Root JSONSchema (exclusive schema union) 40 | * @param OPTIONS Parsing options 41 | * @returns Meta-type 42 | */ 43 | type RecurseOnOneOfSchema< 44 | SUB_SCHEMAS extends readonly JSONSchema[], 45 | ROOT_ONE_OF_SCHEMA extends OneOfSchema, 46 | OPTIONS extends ParseSchemaOptions, 47 | RESULT = never, 48 | > = SUB_SCHEMAS extends readonly [ 49 | infer SUB_SCHEMAS_HEAD, 50 | ...infer SUB_SCHEMAS_TAIL, 51 | ] 52 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 53 | SUB_SCHEMAS_HEAD extends JSONSchema 54 | ? SUB_SCHEMAS_TAIL extends readonly JSONSchema[] 55 | ? RecurseOnOneOfSchema< 56 | SUB_SCHEMAS_TAIL, 57 | ROOT_ONE_OF_SCHEMA, 58 | OPTIONS, 59 | | RESULT 60 | | M.$Intersect< 61 | ParseSchema, OPTIONS>, 62 | ParseSchema< 63 | MergeSubSchema< 64 | Omit, 65 | SUB_SCHEMAS_HEAD 66 | >, 67 | OPTIONS 68 | > 69 | > 70 | > 71 | : never 72 | : never 73 | : RESULT; 74 | -------------------------------------------------------------------------------- /src/parse-schema/references/external.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | import type { Join, Pop, Split } from "~/type-utils"; 5 | 6 | import type { ParseSchemaOptions } from "../index"; 7 | import type { ReferencingSchema } from "./index"; 8 | import type { ParseReference } from "./utils"; 9 | 10 | /** 11 | * Parse a JSON schema referencing an external JSON schema (through its `$ref` property) to a meta-type. 12 | * @param SCHEMA JSONSchema 13 | * @param OPTIONS Parsing options 14 | * @param REFERENCE_SOURCE JSONSchema 15 | * @param PATH_IN_SOURCE string | undefined 16 | * @returns Meta-type 17 | */ 18 | export type ParseExternalReferenceSchema< 19 | REF_SCHEMA extends ReferencingSchema, 20 | OPTIONS extends ParseSchemaOptions, 21 | EXTERNAL_REFERENCE_ID extends string, 22 | SUB_PATH extends string | undefined, 23 | > = OPTIONS["references"] extends { 24 | [KEY in EXTERNAL_REFERENCE_ID]: JSONSchema; 25 | } 26 | ? ParseReference< 27 | Omit, 28 | OPTIONS, 29 | OPTIONS["references"][EXTERNAL_REFERENCE_ID], 30 | SUB_PATH 31 | > 32 | : OPTIONS extends { rootSchema: IdSchema } 33 | ? ParseExternalReferenceWithoutDirectorySchema< 34 | Omit, 35 | OPTIONS, 36 | EXTERNAL_REFERENCE_ID, 37 | SUB_PATH 38 | > 39 | : M.Never; 40 | 41 | /** 42 | * Returns the directory of a reference. 43 | * @param REFERENCE String 44 | * @returns String 45 | * @example 46 | * type Directory = ParseDirectory<"some/directory/file"> 47 | * // => "some/directory" 48 | */ 49 | type ParseDirectory = Join< 50 | Pop>, 51 | "/" 52 | >; 53 | 54 | /** 55 | * JSON schema that can be referenced through its id 56 | * @example 57 | * const idSchema = { 58 | * $id: "my-schema", 59 | * type: "string", 60 | * } 61 | */ 62 | type IdSchema = JSONSchema & { $id: string }; 63 | 64 | /** 65 | * Parse a JSON schema referencing an external JSON schema (through its `$ref` property - no directory) to a meta-type. 66 | * @param SUB_SCHEMA JSONSchema 67 | * @param OPTIONS Parsing options 68 | * @param EXTERNAL_REFERENCE_ID String 69 | * @param DEFINITION String 70 | * @param SUB_PATH String | undefined 71 | * @returns Meta-type 72 | */ 73 | type ParseExternalReferenceWithoutDirectorySchema< 74 | SUB_SCHEMA extends JSONSchema, 75 | OPTIONS extends ParseSchemaOptions & { rootSchema: IdSchema }, 76 | EXTERNAL_REFERENCE_ID extends string, 77 | SUB_PATH extends string | undefined, 78 | DIRECTORY extends string = ParseDirectory, 79 | COMPLETE_REFERENCE extends string = Join< 80 | [DIRECTORY, EXTERNAL_REFERENCE_ID], 81 | "/" 82 | >, 83 | > = COMPLETE_REFERENCE extends keyof OPTIONS["references"] 84 | ? ParseReference< 85 | SUB_SCHEMA, 86 | OPTIONS, 87 | OPTIONS["references"][COMPLETE_REFERENCE], 88 | SUB_PATH 89 | > 90 | : M.Never; 91 | -------------------------------------------------------------------------------- /src/parse-schema/references/external.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | import { ajv } from "../ajv.util.test"; 6 | 7 | describe("References", () => { 8 | const definitionsSchema = { 9 | $id: "http://example.com/schemas/defs.json", 10 | definitions: { 11 | int: { type: "integer" }, 12 | str: { type: "string" }, 13 | }, 14 | } as const; 15 | 16 | ajv.addSchema(definitionsSchema); 17 | 18 | const personSchemaReference = { 19 | $id: "http://example.com/schemas/person.json", 20 | type: "object", 21 | properties: { 22 | name: { type: "string" }, 23 | age: { type: "integer" }, 24 | }, 25 | required: ["name"], 26 | additionalProperties: false, 27 | } as const; 28 | 29 | type ExpectedPerson = { age?: number | undefined; name: string }; 30 | 31 | ajv.addSchema(personSchemaReference); 32 | 33 | describe("Default case", () => { 34 | const userSchema = { 35 | $ref: "http://example.com/schemas/person.json", 36 | } as const; 37 | 38 | type User = FromSchema< 39 | typeof userSchema, 40 | { references: [typeof personSchemaReference] } 41 | >; 42 | let userInstance: User; 43 | 44 | const assertUser: A.Equals = 1; 45 | assertUser; 46 | 47 | it("accepts a valid user", () => { 48 | userInstance = { name: "Ryan Gosling", age: 42 }; 49 | expect(ajv.validate(userSchema, userInstance)).toBe(true); 50 | }); 51 | 52 | it("rejects an invalid person", () => { 53 | // @ts-expect-error 54 | userInstance = { name: 42, age: "42" }; 55 | expect(ajv.validate(userSchema, userInstance)).toBe(false); 56 | }); 57 | }); 58 | 59 | describe("Re-using directory", () => { 60 | const userSchema = { 61 | $id: "http://example.com/schemas/user.json", 62 | $ref: "person.json", 63 | } as const; 64 | 65 | type User = FromSchema< 66 | typeof userSchema, 67 | { references: [typeof personSchemaReference] } 68 | >; 69 | let userInstance: User; 70 | 71 | const assertUser: A.Equals = 1; 72 | assertUser; 73 | 74 | it("accepts a valid user", () => { 75 | userInstance = { name: "Ryan Gosling", age: 42 }; 76 | expect(ajv.validate(userSchema, userInstance)).toBe(true); 77 | }); 78 | 79 | it("rejects an invalid person", () => { 80 | // @ts-expect-error 81 | userInstance = { name: 42, age: "42" }; 82 | expect(ajv.validate(userSchema, userInstance)).toBe(false); 83 | }); 84 | }); 85 | 86 | describe("Accessing nested property (absolute path)", () => { 87 | const userSchema = { 88 | type: "object", 89 | properties: { 90 | name: { $ref: "http://example.com/schemas/defs.json#/definitions/str" }, 91 | age: { $ref: "http://example.com/schemas/defs.json#/definitions/int" }, 92 | }, 93 | required: ["name"], 94 | additionalProperties: false, 95 | } as const; 96 | 97 | type User = FromSchema< 98 | typeof userSchema, 99 | { references: [typeof definitionsSchema] } 100 | >; 101 | let userInstance: User; 102 | 103 | const assertUser: A.Equals = 1; 104 | assertUser; 105 | 106 | it("accepts a valid user", () => { 107 | userInstance = { name: "Ryan Gosling", age: 42 }; 108 | expect(ajv.validate(userSchema, userInstance)).toBe(true); 109 | }); 110 | 111 | it("rejects an invalid person", () => { 112 | // @ts-expect-error 113 | userInstance = { name: 42, age: "42" }; 114 | expect(ajv.validate(userSchema, userInstance)).toBe(false); 115 | }); 116 | }); 117 | 118 | describe("Accessing nested property (relative path)", () => { 119 | const userSchema = { 120 | $id: "http://example.com/schemas/nested-user.json", 121 | type: "object", 122 | properties: { 123 | name: { $ref: "defs.json#/definitions/str" }, 124 | age: { $ref: "defs.json#/definitions/int" }, 125 | }, 126 | required: ["name"], 127 | additionalProperties: false, 128 | } as const; 129 | 130 | type User = FromSchema< 131 | typeof userSchema, 132 | { references: [typeof definitionsSchema] } 133 | >; 134 | let userInstance: User; 135 | 136 | const assertUser: A.Equals = 1; 137 | assertUser; 138 | 139 | it("accepts a valid user", () => { 140 | userInstance = { name: "Ryan Gosling", age: 42 }; 141 | expect(ajv.validate(userSchema, userInstance)).toBe(true); 142 | }); 143 | 144 | it("rejects an invalid person", () => { 145 | // @ts-expect-error 146 | userInstance = { name: 42, age: "42" }; 147 | expect(ajv.validate(userSchema, userInstance)).toBe(false); 148 | }); 149 | }); 150 | 151 | describe("Along definition", () => { 152 | const userSchema = { 153 | $id: "http://example.com/schemas/definitions-user.json", 154 | type: "object", 155 | properties: { 156 | name: { $ref: "defs.json#/definitions/str" }, 157 | age: { $ref: "#/definitions/int" }, 158 | }, 159 | required: ["name"], 160 | additionalProperties: false, 161 | definitions: { 162 | int: { type: "number" }, 163 | }, 164 | } as const; 165 | 166 | type User = FromSchema< 167 | typeof userSchema, 168 | { references: [typeof definitionsSchema] } 169 | >; 170 | let userInstance: User; 171 | 172 | const assertUser: A.Equals = 1; 173 | assertUser; 174 | 175 | it("accepts a valid user", () => { 176 | userInstance = { name: "Ryan Gosling", age: 42 }; 177 | expect(ajv.validate(userSchema, userInstance)).toBe(true); 178 | }); 179 | 180 | it("rejects an invalid person", () => { 181 | // @ts-expect-error 182 | userInstance = { name: 42, age: "42" }; 183 | expect(ajv.validate(userSchema, userInstance)).toBe(false); 184 | }); 185 | }); 186 | 187 | describe("In combinaison with keywords", () => { 188 | const personWithAgeSchema = { 189 | $ref: "http://example.com/schemas/person.json", 190 | required: ["age"], 191 | additionalProperties: true, 192 | } as const; 193 | 194 | type PersonWithAge = FromSchema< 195 | typeof personWithAgeSchema, 196 | { references: [typeof personSchemaReference] } 197 | >; 198 | let personWithAgeInstance: PersonWithAge; 199 | 200 | const assertPersonWithAge: A.Equals< 201 | PersonWithAge, 202 | Required 203 | > = 1; 204 | assertPersonWithAge; 205 | 206 | it("rejects additional properties (both additionalProperties apply)", () => { 207 | personWithAgeInstance = { 208 | name: "judy", 209 | age: 42, 210 | // @ts-expect-error 211 | additionalProp: true, 212 | }; 213 | expect(ajv.validate(personWithAgeSchema, personWithAgeInstance)).toBe( 214 | false, 215 | ); 216 | }); 217 | 218 | it("rejects if firstName OR last name misses (both required apply)", () => { 219 | // @ts-expect-error 220 | personWithAgeInstance = { name: "judy" }; 221 | expect(ajv.validate(personWithAgeSchema, personWithAgeInstance)).toBe( 222 | false, 223 | ); 224 | 225 | // @ts-expect-error 226 | personWithAgeInstance = { age: 42 }; 227 | expect(ajv.validate(personWithAgeSchema, personWithAgeInstance)).toBe( 228 | false, 229 | ); 230 | }); 231 | }); 232 | 233 | describe("Refs using other refs", () => { 234 | const personWithAgeSchema = { 235 | $id: "http://example.com/schemas/person-with-age.json", 236 | $ref: "http://example.com/schemas/person.json", 237 | required: ["age"], 238 | } as const; 239 | 240 | const personWithAgeSchema2 = { 241 | $ref: "http://example.com/schemas/person-with-age.json", 242 | } as const; 243 | 244 | type PersonWithAge = FromSchema< 245 | typeof personWithAgeSchema2, 246 | { references: [typeof personSchemaReference, typeof personWithAgeSchema] } 247 | >; 248 | let personWithAgeInstance: PersonWithAge; 249 | 250 | const assertPersonWithAge: A.Equals< 251 | PersonWithAge, 252 | Required 253 | > = 1; 254 | assertPersonWithAge; 255 | 256 | ajv.addSchema(personWithAgeSchema); 257 | 258 | it("accepts a valid person", () => { 259 | personWithAgeInstance = { name: "judy", age: 42 }; 260 | expect(ajv.validate(personWithAgeSchema2, personWithAgeInstance)).toBe( 261 | true, 262 | ); 263 | }); 264 | 265 | it("rejects an invalid person", () => { 266 | personWithAgeInstance = { 267 | name: "judy", 268 | // @ts-expect-error 269 | age: true, 270 | }; 271 | expect(ajv.validate(personWithAgeSchema2, personWithAgeInstance)).toBe( 272 | false, 273 | ); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /src/parse-schema/references/index.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema } from "~/definitions"; 2 | import type { Split } from "~/type-utils/split"; 3 | 4 | import type { ParseSchemaOptions } from "../index"; 5 | import type { ParseExternalReferenceSchema } from "./external"; 6 | import type { ParseInternalReferenceSchema } from "./internal"; 7 | 8 | /** 9 | * JSON schemas referencing other schemas 10 | */ 11 | export type ReferencingSchema = JSONSchema & { 12 | $ref: string; 13 | }; 14 | 15 | /** 16 | * Recursively parses a JSON referencing another schema to a meta-type. 17 | * 18 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 19 | * @param REFERENCING_SCHEMA JSONSchema (referencing) 20 | * @param OPTIONS Parsing options 21 | * @returns Meta-type 22 | */ 23 | export type ParseReferenceSchema< 24 | REFERENCING_SCHEMA extends ReferencingSchema, 25 | OPTIONS extends ParseSchemaOptions, 26 | REFERENCE_ID_AND_PATH extends string[] = Split< 27 | REFERENCING_SCHEMA["$ref"], 28 | "#" 29 | >, 30 | > = REFERENCE_ID_AND_PATH[0] extends "" 31 | ? ParseInternalReferenceSchema< 32 | REFERENCING_SCHEMA, 33 | OPTIONS, 34 | REFERENCE_ID_AND_PATH[1] 35 | > 36 | : ParseExternalReferenceSchema< 37 | REFERENCING_SCHEMA, 38 | OPTIONS, 39 | REFERENCE_ID_AND_PATH[0], 40 | REFERENCE_ID_AND_PATH[1] 41 | >; 42 | -------------------------------------------------------------------------------- /src/parse-schema/references/internal.ts: -------------------------------------------------------------------------------- 1 | import type { ParseSchemaOptions } from "../index"; 2 | import type { ReferencingSchema } from "./index"; 3 | import type { ParseReference } from "./utils"; 4 | 5 | /** 6 | * Recursively parses a JSON schema referencing another part of its root JSON schema (through its `$ref` property) to a meta-type. 7 | * @param REFERENCING_SCHEMA JSONSchema (referencing) 8 | * @param OPTIONS Parsing options 9 | * @param DEFINITION String 10 | * @returns Meta-type 11 | */ 12 | export type ParseInternalReferenceSchema< 13 | REFERENCING_SCHEMA extends ReferencingSchema, 14 | OPTIONS extends ParseSchemaOptions, 15 | PATH extends string, 16 | > = ParseReference< 17 | Omit, 18 | OPTIONS, 19 | OPTIONS["rootSchema"], 20 | PATH 21 | >; 22 | -------------------------------------------------------------------------------- /src/parse-schema/references/internal.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "../ajv.util.test"; 4 | 5 | describe("Definitions", () => { 6 | describe("Default case", () => { 7 | const personSchema = { 8 | type: "object", 9 | properties: { 10 | firstName: { $ref: "#/definitions/name" }, 11 | lastName: { $ref: "#/definitions/name" }, 12 | }, 13 | required: ["firstName", "lastName"], 14 | additionalProperties: false, 15 | definitions: { 16 | name: { 17 | type: "string", 18 | }, 19 | }, 20 | } as const; 21 | 22 | type Person = FromSchema; 23 | let personInstance: Person; 24 | 25 | it("accepts a valid person", () => { 26 | personInstance = { firstName: "judy", lastName: "foster" }; 27 | expect(ajv.validate(personSchema, personInstance)).toBe(true); 28 | }); 29 | 30 | it("rejects an invalid person", () => { 31 | // @ts-expect-error 32 | personInstance = { firstName: 42, lastName: true }; 33 | expect(ajv.validate(personSchema, personInstance)).toBe(false); 34 | }); 35 | }); 36 | 37 | describe("Nested path", () => { 38 | const personSchema = { 39 | type: "object", 40 | properties: { 41 | firstName: { $ref: "#/definitions/user/properties/name" }, 42 | lastName: { $ref: "#/definitions/user/properties/name" }, 43 | }, 44 | required: ["firstName", "lastName"], 45 | additionalProperties: false, 46 | definitions: { 47 | user: { 48 | type: "object", 49 | properties: { 50 | name: { type: "string" }, 51 | }, 52 | }, 53 | }, 54 | } as const; 55 | 56 | type Person = FromSchema; 57 | 58 | let personInstance: Person; 59 | 60 | it("accepts a valid person", () => { 61 | personInstance = { firstName: "judy", lastName: "foster" }; 62 | expect(ajv.validate(personSchema, personInstance)).toBe(true); 63 | }); 64 | 65 | it("rejects an invalid person", () => { 66 | // @ts-expect-error 67 | personInstance = { firstName: 42, lastName: true }; 68 | expect(ajv.validate(personSchema, personInstance)).toBe(false); 69 | }); 70 | }); 71 | 72 | describe("In combinaison with keywords", () => { 73 | const judySchema = { 74 | type: "object", 75 | properties: { 76 | judy: { 77 | $ref: "#/definitions/person", 78 | required: ["lastName"], 79 | additionalProperties: true, 80 | }, 81 | }, 82 | required: ["judy"], 83 | additionalProperties: false, 84 | definitions: { 85 | person: { 86 | type: "object", 87 | properties: { 88 | firstName: { type: "string" }, 89 | lastName: { type: "string" }, 90 | }, 91 | required: ["firstName"], 92 | additionalProperties: false, 93 | }, 94 | }, 95 | } as const; 96 | 97 | type Judy = FromSchema; 98 | let judyInstance: Judy; 99 | 100 | it("rejects additional properties (both additionalProperties apply)", () => { 101 | judyInstance = { 102 | judy: { 103 | firstName: "judy", 104 | lastName: "foster", 105 | // @ts-expect-error 106 | additionalProp: true, 107 | }, 108 | }; 109 | expect(ajv.validate(judySchema, judyInstance)).toBe(false); 110 | }); 111 | 112 | it("rejects if firstName OR last name misses (both required apply)", () => { 113 | judyInstance = { 114 | // @ts-expect-error 115 | judy: { firstName: "judy" }, 116 | }; 117 | expect(ajv.validate(judySchema, judyInstance)).toBe(false); 118 | 119 | judyInstance = { 120 | // @ts-expect-error 121 | judy: { lastName: "foster" }, 122 | }; 123 | expect(ajv.validate(judySchema, judyInstance)).toBe(false); 124 | }); 125 | }); 126 | 127 | describe("Definitions using other definitions", () => { 128 | const judySchema = { 129 | type: "object", 130 | properties: { 131 | judy: { $ref: "#/definitions/person" }, 132 | }, 133 | required: ["judy"], 134 | additionalProperties: false, 135 | definitions: { 136 | name: { type: "string" }, 137 | person: { 138 | type: "object", 139 | properties: { 140 | firstName: { $ref: "#/definitions/name" }, 141 | lastName: { $ref: "#/definitions/name" }, 142 | }, 143 | additionalProperties: false, 144 | }, 145 | }, 146 | } as const; 147 | 148 | type Judy = FromSchema; 149 | let judyInstance: Judy; 150 | 151 | it("accepts a valid person", () => { 152 | judyInstance = { judy: { firstName: "judy", lastName: "foster" } }; 153 | expect(ajv.validate(judySchema, judyInstance)).toBe(true); 154 | }); 155 | 156 | it("rejects an invalid person", () => { 157 | judyInstance = { 158 | judy: { 159 | firstName: "judy", 160 | // @ts-expect-error 161 | lastName: true, 162 | }, 163 | }; 164 | expect(ajv.validate(judySchema, judyInstance)).toBe(false); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /src/parse-schema/references/utils.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema } from "~/definitions"; 4 | import type { DeepGet, Split, Tail } from "~/type-utils"; 5 | 6 | import type { ParseSchema, ParseSchemaOptions } from "../index"; 7 | import type { MergeSubSchema } from "../utils"; 8 | 9 | /** 10 | * Recursively parses a referencing JSON schema to a meta-type, finding its reference in a reference source JSON schema (from options or definitions). 11 | * @param SCHEMA JSONSchema 12 | * @param OPTIONS Parsing options 13 | * @param REFERENCE_SOURCE JSONSchema 14 | * @param PATH_IN_SOURCE string | undefined 15 | * @returns Meta-type 16 | */ 17 | export type ParseReference< 18 | SCHEMA extends JSONSchema, 19 | OPTIONS extends ParseSchemaOptions, 20 | REFERENCE_SOURCE extends JSONSchema, 21 | PATH_IN_SOURCE extends string | undefined, 22 | MATCHING_REFERENCE extends JSONSchema = PATH_IN_SOURCE extends string 23 | ? // Tail is needed to remove initial "" from split path 24 | DeepGet>, false> 25 | : REFERENCE_SOURCE, 26 | > = M.$Intersect< 27 | ParseSchema, 28 | ParseSchema, OPTIONS> 29 | >; 30 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.boolean.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Boolean schemas", () => { 6 | const booleanSchema = { type: "boolean" } as const; 7 | 8 | type Boolean = FromSchema; 9 | let booleanInst: Boolean; 10 | 11 | it("accepts boolean value", () => { 12 | booleanInst = true; 13 | expect(ajv.validate(booleanSchema, booleanInst)).toBe(true); 14 | 15 | booleanInst = false; 16 | expect(ajv.validate(booleanSchema, booleanInst)).toBe(true); 17 | }); 18 | 19 | it("rejects other values", () => { 20 | // @ts-expect-error 21 | booleanInst = "true"; 22 | expect(ajv.validate(booleanSchema, booleanInst)).toBe(false); 23 | 24 | // @ts-expect-error 25 | booleanInst = 42; 26 | expect(ajv.validate(booleanSchema, booleanInst)).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.integer.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Integer schemas", () => { 6 | const integerSchema = { type: "integer" } as const; 7 | 8 | type Number = FromSchema; 9 | let integerInstance: Number; 10 | 11 | it("accepts any integer value", () => { 12 | integerInstance = 42; 13 | expect(ajv.validate(integerSchema, integerInstance)).toBe(true); 14 | }); 15 | 16 | it("rejects other values", () => { 17 | // @ts-expect-error 18 | integerInstance = "not a number"; 19 | expect(ajv.validate(integerSchema, integerInstance)).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.null.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Null schema", () => { 6 | const nullSchema = { type: "null" } as const; 7 | 8 | type Null = FromSchema; 9 | let nullInstance: Null; 10 | 11 | it("accepts null const", () => { 12 | nullInstance = null; 13 | expect(ajv.validate(nullSchema, nullInstance)).toBe(true); 14 | }); 15 | 16 | it("rejects other values", () => { 17 | // @ts-expect-error 18 | nullInstance = "not null"; 19 | expect(ajv.validate(nullSchema, nullInstance)).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.number.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("Number schemas", () => { 6 | const numberSchema = { type: "number" } as const; 7 | 8 | type Number = FromSchema; 9 | let numberInstance: Number; 10 | 11 | it("accepts any number value", () => { 12 | numberInstance = 42; 13 | expect(ajv.validate(numberSchema, numberInstance)).toBe(true); 14 | }); 15 | 16 | it("rejects other values", () => { 17 | // @ts-expect-error 18 | numberInstance = ["not", "a", "number"]; 19 | expect(ajv.validate(numberSchema, numberInstance)).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.string.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | import { ajv } from "./ajv.util.test"; 4 | 5 | describe("String schemas", () => { 6 | const stringSchema = { type: "string" } as const; 7 | 8 | type String = FromSchema; 9 | let stringInstance: String; 10 | 11 | it("accepts any string value", () => { 12 | stringInstance = "apples"; 13 | expect(ajv.validate(stringSchema, stringInstance)).toBe(true); 14 | }); 15 | 16 | it("rejects other values", () => { 17 | // @ts-expect-error 18 | stringInstance = 42; 19 | expect(ajv.validate(stringSchema, stringInstance)).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/parse-schema/singleType.ts: -------------------------------------------------------------------------------- 1 | import type { M } from "ts-algebra"; 2 | 3 | import type { JSONSchema, JSONSchemaType } from "~/definitions"; 4 | 5 | import type { ArrayOrTupleSchema, ParseArrayOrTupleSchema } from "./array"; 6 | import type { ParseSchemaOptions } from "./index"; 7 | import type { ObjectSchema, ParseObjectSchema } from "./object"; 8 | 9 | /** 10 | * JSON schemas of a single type 11 | * @example 12 | * const singleTypeSchema = { 13 | * type: "string" 14 | * } 15 | */ 16 | export type SingleTypeSchema = JSONSchema & Readonly<{ type: JSONSchemaType }>; 17 | 18 | /** 19 | * Recursively parses a single type JSON schema to a meta-type. 20 | * 21 | * Check the [ts-algebra documentation](https://github.com/ThomasAribart/ts-algebra) for more informations on how meta-types work. 22 | * @param SINGLE_TYPE_SCHEMA JSONSchema (single type) 23 | * @param OPTIONS Parsing options 24 | * @returns Meta-type 25 | */ 26 | export type ParseSingleTypeSchema< 27 | SINGLE_TYPE_SCHEMA extends SingleTypeSchema, 28 | OPTIONS extends ParseSchemaOptions, 29 | > = SINGLE_TYPE_SCHEMA extends Readonly<{ type: "null" }> 30 | ? M.Primitive 31 | : SINGLE_TYPE_SCHEMA extends Readonly<{ type: "boolean" }> 32 | ? M.Primitive 33 | : SINGLE_TYPE_SCHEMA extends Readonly<{ type: "integer" }> 34 | ? M.Primitive 35 | : SINGLE_TYPE_SCHEMA extends Readonly<{ type: "number" }> 36 | ? M.Primitive 37 | : SINGLE_TYPE_SCHEMA extends Readonly<{ type: "string" }> 38 | ? M.Primitive 39 | : SINGLE_TYPE_SCHEMA extends ArrayOrTupleSchema 40 | ? ParseArrayOrTupleSchema 41 | : SINGLE_TYPE_SCHEMA extends ObjectSchema 42 | ? ParseObjectSchema 43 | : M.Never; 44 | -------------------------------------------------------------------------------- /src/parse-schema/utils.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema } from "~/definitions"; 2 | 3 | /** 4 | * Resets `additionalItems` property from a sub-schema before merging it to a parent schema 5 | */ 6 | type RemoveInvalidAdditionalItems = 7 | SCHEMA extends Readonly<{ items: JSONSchema | readonly JSONSchema[] }> 8 | ? SCHEMA extends Readonly<{ additionalItems: JSONSchema }> 9 | ? SCHEMA 10 | : SCHEMA & Readonly<{ additionalItems: true }> 11 | : SCHEMA extends boolean 12 | ? SCHEMA 13 | : Omit; 14 | 15 | /** 16 | * Resets `additionalProperties` and `properties` from a sub-schema before merging it to a parent schema 17 | */ 18 | type RemoveInvalidAdditionalProperties = 19 | SCHEMA extends Readonly<{ additionalProperties: JSONSchema }> 20 | ? SCHEMA extends Readonly<{ 21 | properties: Readonly>; 22 | }> 23 | ? SCHEMA 24 | : SCHEMA & Readonly<{ properties: {} }> 25 | : SCHEMA extends boolean 26 | ? SCHEMA 27 | : Omit; 28 | 29 | /** 30 | * Merges a sub-schema into a parent schema. 31 | * 32 | * Resets `properties`, `additionalProperties`, `required`, `additionalItems` if required. 33 | * @param PARENT_SCHEMA JSONSchema 34 | * @param SUB_SCHEMA JSONSchema 35 | * @returns JSONSchema 36 | */ 37 | export type MergeSubSchema< 38 | PARENT_SCHEMA extends JSONSchema, 39 | SUB_SCHEMA extends JSONSchema, 40 | CLEANED_SUB_SCHEMA extends JSONSchema = RemoveInvalidAdditionalProperties< 41 | RemoveInvalidAdditionalItems 42 | >, 43 | > = Omit< 44 | PARENT_SCHEMA, 45 | | keyof CLEANED_SUB_SCHEMA 46 | | "additionalProperties" 47 | | "patternProperties" 48 | | "unevaluatedProperties" 49 | | "required" 50 | | "additionalItems" 51 | > & 52 | CLEANED_SUB_SCHEMA; 53 | -------------------------------------------------------------------------------- /src/tests/readme/allOf.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const addressSchema = { 6 | type: "object", 7 | allOf: [ 8 | { 9 | properties: { 10 | address: { type: "string" }, 11 | city: { type: "string" }, 12 | state: { type: "string" }, 13 | }, 14 | required: ["address", "city", "state"], 15 | }, 16 | { 17 | properties: { 18 | type: { enum: ["residential", "business"] }, 19 | }, 20 | }, 21 | ], 22 | } as const; 23 | 24 | type ReceivedAddress = FromSchema; 25 | type ExpectedAddress = { 26 | [x: string]: unknown; 27 | address: string; 28 | city: string; 29 | state: string; 30 | type?: "residential" | "business"; 31 | }; 32 | 33 | type AssertAddress = A.Equals; 34 | const assertAddress: AssertAddress = 1; 35 | assertAddress; 36 | -------------------------------------------------------------------------------- /src/tests/readme/anyOf.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | // Regular 6 | 7 | const regularAnyOfSchema = { 8 | anyOf: [ 9 | { type: "string" }, 10 | { 11 | type: "array", 12 | items: { type: "string" }, 13 | }, 14 | ], 15 | } as const; 16 | 17 | type ReceivedRegularAnyOf = FromSchema; 18 | 19 | type ExpectedRegularAnyOf = string | string[]; 20 | 21 | type AssertRegularAnyOf = A.Equals; 22 | const assertRegularAnyOf: AssertRegularAnyOf = 1; 23 | assertRegularAnyOf; 24 | 25 | // Factored 26 | 27 | const factoredAnyOfSchema = { 28 | type: "object", 29 | properties: { 30 | bool: { type: "boolean" }, 31 | }, 32 | required: ["bool"], 33 | anyOf: [ 34 | { 35 | properties: { 36 | str: { type: "string" }, 37 | }, 38 | required: ["str"], 39 | }, 40 | { 41 | properties: { 42 | num: { type: "number" }, 43 | }, 44 | }, 45 | ], 46 | } as const; 47 | 48 | type ReceivedFactoredAnyOf = FromSchema; 49 | 50 | type ExpectedFactoredAnyOf = 51 | | { 52 | [x: string]: unknown; 53 | bool: boolean; 54 | str: string; 55 | } 56 | | { 57 | [x: string]: unknown; 58 | bool: boolean; 59 | num?: number; 60 | }; 61 | 62 | type AssertFactoredAnyOf = A.Equals< 63 | ReceivedFactoredAnyOf, 64 | ExpectedFactoredAnyOf 65 | >; 66 | const assertFactoredAnyOf: AssertFactoredAnyOf = 1; 67 | assertFactoredAnyOf; 68 | -------------------------------------------------------------------------------- /src/tests/readme/array.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const arraySchema = { 6 | type: "array", 7 | items: { type: "string" }, 8 | } as const; 9 | 10 | type ReceivedArray = FromSchema; 11 | type ExpectedArray = string[]; 12 | 13 | type AssertArray = A.Equals; 14 | const assertArray: AssertArray = 1; 15 | assertArray; 16 | -------------------------------------------------------------------------------- /src/tests/readme/const.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const constSchema = { 6 | const: "foo", 7 | } as const; 8 | 9 | type ReceivedConst = FromSchema; 10 | type ExpectedConst = "foo"; 11 | 12 | type AssertConst = A.Equals; 13 | const assertConst: AssertConst = 1; 14 | assertConst; 15 | -------------------------------------------------------------------------------- /src/tests/readme/definitions.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const userSchema = { 6 | type: "object", 7 | properties: { 8 | name: { $ref: "#/definitions/name" }, 9 | age: { $ref: "#/definitions/age" }, 10 | }, 11 | required: ["name", "age"], 12 | additionalProperties: false, 13 | definitions: { 14 | name: { type: "string" }, 15 | age: { type: "integer" }, 16 | }, 17 | } as const; 18 | 19 | type ReceivedUser = FromSchema; 20 | type ExpectedUser = { 21 | name: string; 22 | age: number; 23 | }; 24 | 25 | type AssertUser = A.Equals; 26 | const assertUser: AssertUser = 1; 27 | assertUser; 28 | -------------------------------------------------------------------------------- /src/tests/readme/deserialization.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const userSchema = { 6 | type: "object", 7 | properties: { 8 | name: { type: "string" }, 9 | email: { 10 | type: "string", 11 | format: "email", 12 | }, 13 | birthDate: { 14 | type: "string", 15 | format: "date-time", 16 | }, 17 | }, 18 | required: ["name", "email", "birthDate"], 19 | additionalProperties: false, 20 | } as const; 21 | 22 | type Email = string & { brand: "email" }; 23 | 24 | type ReceivedUser = FromSchema< 25 | typeof userSchema, 26 | { 27 | deserialize: [ 28 | { 29 | pattern: { 30 | type: "string"; 31 | format: "email"; 32 | }; 33 | output: Email; 34 | }, 35 | { 36 | pattern: { 37 | type: "string"; 38 | format: "date-time"; 39 | }; 40 | output: Date; 41 | }, 42 | ]; 43 | } 44 | >; 45 | type ExpectedUser = { 46 | name: string; 47 | email: Email; 48 | birthDate: Date; 49 | }; 50 | 51 | type AssertUser = A.Equals; 52 | const assertUser: AssertUser = 1; 53 | assertUser; 54 | -------------------------------------------------------------------------------- /src/tests/readme/enum.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | // Simple enum 6 | 7 | const enumSchema = { 8 | enum: [true, 42, { foo: "bar" }], 9 | } as const; 10 | 11 | type ReceivedEnum = FromSchema; 12 | type ExpectedEnum = true | 42 | { foo: "bar" }; 13 | 14 | type AssertEnum = A.Equals; 15 | const assertEnum: AssertEnum = 1; 16 | assertEnum; 17 | 18 | // TS enum 19 | 20 | enum Food { 21 | Pizza = "pizza", 22 | Taco = "taco", 23 | Fries = "fries", 24 | } 25 | 26 | const foodSchema = { 27 | enum: Object.values(Food), 28 | } as const; 29 | 30 | type ReceivedFood = FromSchema; 31 | type ExpectedFood = Food; 32 | 33 | // Don't know why, A.Equals returns false.. 34 | type AssertFoodLeft = A.Extends; 35 | const assertFoodLeft: AssertFoodLeft = 1; 36 | assertFoodLeft; 37 | type AssertFoodRight = A.Extends; 38 | const assertFoodRight: AssertFoodRight = 1; 39 | assertFoodRight; 40 | -------------------------------------------------------------------------------- /src/tests/readme/extensions.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { ExtendedJSONSchema, FromExtendedSchema } from "~/index"; 4 | 5 | type CustomProps = { 6 | numberType: "int" | "float" | "bigInt"; 7 | }; 8 | 9 | const bigIntSchema = { 10 | type: "number", 11 | numberType: "bigInt", 12 | } as const; 13 | 14 | type AssertExtends = A.Extends< 15 | typeof bigIntSchema, 16 | ExtendedJSONSchema 17 | >; 18 | const assertExtends: AssertExtends = 1; 19 | assertExtends; 20 | 21 | const invalidSchema = { 22 | type: "number", 23 | numberType: "bigIntt", 24 | } as const; 25 | 26 | type AssertNotExtends = A.Extends< 27 | typeof invalidSchema, 28 | ExtendedJSONSchema 29 | >; 30 | const assertNotExtends: AssertNotExtends = 0; 31 | assertNotExtends; 32 | 33 | type BigInt = FromExtendedSchema< 34 | CustomProps, 35 | typeof bigIntSchema, 36 | { 37 | deserialize: [ 38 | { 39 | pattern: { 40 | type: "number"; 41 | numberType: "bigInt"; 42 | }; 43 | output: bigint; 44 | }, 45 | ]; 46 | } 47 | >; 48 | type AssertBigInt = A.Equals; 49 | const assertBigInt: AssertBigInt = 1; 50 | assertBigInt; 51 | 52 | const nestedSchema = { 53 | type: "object", 54 | properties: { 55 | nested: { 56 | numberType: "bigInt", 57 | }, 58 | }, 59 | required: ["nested"], 60 | additionalProperties: false, 61 | } as const; 62 | 63 | type NestedBigInt = FromExtendedSchema< 64 | CustomProps, 65 | typeof nestedSchema, 66 | { 67 | deserialize: [ 68 | { 69 | pattern: { 70 | numberType: "bigInt"; 71 | }; 72 | output: bigint; 73 | }, 74 | ]; 75 | } 76 | >; 77 | type AssertNestedBigInt = A.Equals; 78 | const assertNestedBigInt: AssertNestedBigInt = 1; 79 | assertNestedBigInt; 80 | -------------------------------------------------------------------------------- /src/tests/readme/ifThenElse.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | enum DogBreed { 6 | poodle = "poodle", 7 | } 8 | 9 | enum CatBreed { 10 | persan = "persan", 11 | } 12 | 13 | const petSchema = { 14 | type: "object", 15 | properties: { 16 | animal: { enum: ["cat", "dog"] }, 17 | dogBreed: { enum: Object.values(DogBreed) }, 18 | catBreed: { enum: Object.values(CatBreed) }, 19 | }, 20 | required: ["animal"], 21 | additionalProperties: false, 22 | if: { 23 | properties: { 24 | animal: { const: "dog" }, 25 | }, 26 | }, 27 | then: { 28 | required: ["dogBreed"], 29 | not: { required: ["catBreed"] }, 30 | }, 31 | else: { 32 | required: ["catBreed"], 33 | not: { required: ["dogBreed"] }, 34 | }, 35 | } as const; 36 | 37 | type ReceivedPet = FromSchema< 38 | typeof petSchema, 39 | { parseIfThenElseKeywords: true } 40 | >; 41 | type ExpectedPet = 42 | | { 43 | animal: "dog"; 44 | dogBreed: DogBreed; 45 | catBreed?: CatBreed | undefined; 46 | } 47 | | { 48 | animal: "cat"; 49 | catBreed: CatBreed; 50 | dogBreed?: DogBreed | undefined; 51 | }; 52 | 53 | type AssertPet = A.Equals; 54 | const assertPet: AssertPet = 1; 55 | assertPet; 56 | -------------------------------------------------------------------------------- /src/tests/readme/intro.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | import { asConst } from "~/utils/asConst"; 5 | 6 | // Dog 7 | 8 | const dogSchema = { 9 | type: "object", 10 | properties: { 11 | name: { type: "string" }, 12 | age: { type: "integer" }, 13 | hobbies: { type: "array", items: { type: "string" } }, 14 | favoriteFood: { enum: ["pizza", "taco", "fries"] }, 15 | }, 16 | required: ["name", "age"], 17 | } as const; 18 | 19 | type ReceivedDog = FromSchema; 20 | type ExpectedDog = { 21 | [key: string]: unknown; 22 | hobbies?: string[]; 23 | favoriteFood?: "pizza" | "taco" | "fries"; 24 | name: string; 25 | age: number; 26 | }; 27 | 28 | type AssertDog = A.Equals; 29 | const assertDog: AssertDog = 1; 30 | assertDog; 31 | 32 | // With asConst util 33 | 34 | const dogSchemaB = asConst({ 35 | type: "object", 36 | properties: { 37 | name: { type: "string" }, 38 | age: { type: "integer" }, 39 | hobbies: { type: "array", items: { type: "string" } }, 40 | favoriteFood: { enum: ["pizza", "taco", "fries"] }, 41 | }, 42 | required: ["name", "age"], 43 | }); 44 | 45 | type ReceivedDog2 = FromSchema; 46 | 47 | type AssertDog2 = A.Equals; 48 | const assertDog2: AssertDog2 = 1; 49 | assertDog2; 50 | 51 | // Address (impossible schema) 52 | 53 | const addressSchema = { 54 | type: "object", 55 | allOf: [ 56 | { 57 | properties: { 58 | street: { type: "string" }, 59 | city: { type: "string" }, 60 | state: { type: "string" }, 61 | }, 62 | required: ["street", "city", "state"], 63 | }, 64 | { 65 | properties: { 66 | type: { enum: ["residential", "business"] }, 67 | }, 68 | }, 69 | ], 70 | additionalProperties: false, 71 | } as const; 72 | 73 | type ReceivedAddress = FromSchema; 74 | type ExpectedAddress = never; 75 | 76 | type AssertAddress = A.Equals; 77 | const assertAddress: AssertAddress = 1; 78 | assertAddress; 79 | -------------------------------------------------------------------------------- /src/tests/readme/not.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | // Tuple 6 | 7 | const tupleSchema = { 8 | type: "array", 9 | items: [{ const: 1 }, { const: 2 }], 10 | additionalItems: false, 11 | not: { 12 | const: [1], 13 | }, 14 | } as const; 15 | 16 | type ReceivedTuple = FromSchema; 17 | type ExpectedTuple = [] | [1, 2]; 18 | 19 | type AssertTuple = A.Equals; 20 | const assertTuple: AssertTuple = 1; 21 | assertTuple; 22 | 23 | // Primitive 24 | 25 | const primitiveTypeSchema = { 26 | not: { 27 | type: ["array", "object"], 28 | }, 29 | } as const; 30 | 31 | type ReceivedPrimitive = FromSchema< 32 | typeof primitiveTypeSchema, 33 | { parseNotKeyword: true } 34 | >; 35 | type ExpectedPrimitive = null | boolean | number | string; 36 | 37 | type AssertPrimitive = A.Equals; 38 | const assertPrimitive: AssertPrimitive = 1; 39 | assertPrimitive; 40 | 41 | // Propagatable object exclusion 42 | 43 | const petSchema = { 44 | type: "object", 45 | properties: { 46 | animal: { enum: ["cat", "dog", "boat"] }, 47 | }, 48 | not: { 49 | properties: { animal: { const: "boat" } }, 50 | }, 51 | required: ["animal"], 52 | additionalProperties: false, 53 | } as const; 54 | 55 | type ReceivedPet = FromSchema; 56 | type ExpectedPet = { animal: "cat" | "dog" }; 57 | 58 | type AssertPet = A.Equals; 59 | const assertPet: AssertPet = 1; 60 | assertPet; 61 | 62 | // Propagatable 63 | 64 | const petSchema2 = { 65 | type: "object", 66 | properties: { 67 | animal: { enum: ["cat", "dog"] }, 68 | color: { enum: ["black", "brown", "white"] }, 69 | }, 70 | not: { 71 | const: { animal: "cat", color: "white" }, 72 | }, 73 | required: ["animal", "color"], 74 | additionalProperties: false, 75 | } as const; 76 | 77 | type ReceivedPet2 = FromSchema; 78 | type ExpectedPet2 = { 79 | animal: "cat" | "dog"; 80 | color: "black" | "brown" | "white"; 81 | }; 82 | 83 | type AssertPet2 = A.Equals; 84 | const assertPet2: AssertPet2 = 1; 85 | assertPet2; 86 | 87 | // Number 88 | 89 | const oddNumberSchema = { 90 | type: "number", 91 | not: { multipleOf: 2 }, 92 | } as const; 93 | 94 | type ReceivedOddNumber = FromSchema< 95 | typeof oddNumberSchema, 96 | { parseNotKeyword: true } 97 | >; 98 | type ExpectedOddNumber = number; 99 | 100 | type AssertOddNumber = A.Equals; 101 | const assertOddNumber: AssertOddNumber = 1; 102 | assertOddNumber; 103 | 104 | // Incorrect 105 | 106 | const incorrectSchema = { 107 | type: "number", 108 | not: { bogus: "option" }, 109 | } as const; 110 | 111 | type ReceivedIncorrect = FromSchema< 112 | // @ts-expect-error 113 | typeof incorrectSchema, 114 | { parseNotKeyword: true } 115 | >; 116 | type ExpectedIncorrect = unknown; 117 | 118 | type AssertIncorrect = A.Equals; 119 | const assertIncorrect: AssertIncorrect = 1; 120 | assertIncorrect; 121 | 122 | // Refinment types 123 | 124 | const goodLanguageSchema = { 125 | type: "string", 126 | not: { 127 | enum: ["Bummer", "Silly", "Lazy sod !"], 128 | }, 129 | } as const; 130 | 131 | type ReceivedGoodLanguage = FromSchema< 132 | typeof goodLanguageSchema, 133 | { parseNotKeyword: true } 134 | >; 135 | type ExpectedGoodLanguage = string; 136 | 137 | type AssertGoodLanguage = A.Equals; 138 | const assertGoodLanguage: AssertGoodLanguage = 1; 139 | assertGoodLanguage; 140 | -------------------------------------------------------------------------------- /src/tests/readme/nullable.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const nullableSchema = { 6 | type: "string", 7 | nullable: true, 8 | } as const; 9 | 10 | type ReceivedNullable = FromSchema; 11 | type ExpectedNullable = string | null; 12 | 13 | type AssertNullable = A.Equals; 14 | const assertNullable: AssertNullable = 1; 15 | assertNullable; 16 | -------------------------------------------------------------------------------- /src/tests/readme/object.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | // With additional properties 6 | 7 | const objectWithAdditionalPropertiesSchema = { 8 | type: "object", 9 | properties: { 10 | foo: { type: "string" }, 11 | bar: { type: "number" }, 12 | }, 13 | required: ["foo"], 14 | } as const; 15 | 16 | type ReceivedObjectWithAdditionalProperties = FromSchema< 17 | typeof objectWithAdditionalPropertiesSchema 18 | >; 19 | type ExpectedObjectWithAdditionalProperties = { 20 | [x: string]: unknown; 21 | foo: string; 22 | bar?: number; 23 | }; 24 | 25 | type AssertObjectWithAdditionalProperties = A.Equals< 26 | ReceivedObjectWithAdditionalProperties, 27 | ExpectedObjectWithAdditionalProperties 28 | >; 29 | const assertObjectWithAdditionalProperties: AssertObjectWithAdditionalProperties = 1; 30 | assertObjectWithAdditionalProperties; 31 | 32 | // No additional properties 33 | 34 | const objectWithoutAdditionalPropertiesSchema = { 35 | ...objectWithAdditionalPropertiesSchema, 36 | additionalProperties: false, 37 | } as const; 38 | 39 | type ReceivedObjectWithoutAdditionalProperties = FromSchema< 40 | typeof objectWithoutAdditionalPropertiesSchema 41 | >; 42 | type ExpectedObjectWithoutAdditionalProperties = { 43 | foo: string; 44 | bar?: number; 45 | }; 46 | 47 | type AssertObjectWithoutAdditionalProperties = A.Equals< 48 | ReceivedObjectWithoutAdditionalProperties, 49 | ExpectedObjectWithoutAdditionalProperties 50 | >; 51 | const assertObjectWithoutAdditionalProperties: AssertObjectWithoutAdditionalProperties = 1; 52 | assertObjectWithoutAdditionalProperties; 53 | 54 | // With typed additional properties 55 | 56 | const objectWithTypedAdditionalPropertiesSchema = { 57 | type: "object", 58 | additionalProperties: { 59 | type: "boolean", 60 | }, 61 | patternProperties: { 62 | "^S": { type: "string" }, 63 | "^I": { type: "integer" }, 64 | }, 65 | } as const; 66 | 67 | type ReceivedObjectWithTypedAdditionalProperties = FromSchema< 68 | typeof objectWithTypedAdditionalPropertiesSchema 69 | >; 70 | type ExpectedObjectWithTypedAdditionalProperties = { 71 | [x: string]: string | number | boolean; 72 | }; 73 | 74 | type AssertObjectWithTypedAdditionalProperties = A.Equals< 75 | ReceivedObjectWithTypedAdditionalProperties, 76 | ExpectedObjectWithTypedAdditionalProperties 77 | >; 78 | const assertObjectWithTypedAdditionalProperties: AssertObjectWithTypedAdditionalProperties = 1; 79 | assertObjectWithTypedAdditionalProperties; 80 | 81 | // Mixed schema 82 | 83 | const mixedObjectSchema = { 84 | type: "object", 85 | properties: { 86 | foo: { enum: ["bar", "baz"] }, 87 | }, 88 | additionalProperties: { type: "string" }, 89 | } as const; 90 | 91 | type ReceivedMixedObject = FromSchema; 92 | type ExpectedMixedObject = { [x: string]: unknown; foo?: "bar" | "baz" }; 93 | 94 | type AssertMixedObject = A.Equals; 95 | const assertMixedObject: AssertMixedObject = 1; 96 | assertMixedObject; 97 | 98 | // Unevaluated properties schema 99 | 100 | const closedObjectSchema = { 101 | type: "object", 102 | allOf: [ 103 | { 104 | properties: { 105 | foo: { type: "string" }, 106 | }, 107 | required: ["foo"], 108 | }, 109 | { 110 | properties: { 111 | bar: { type: "number" }, 112 | }, 113 | }, 114 | ], 115 | unevaluatedProperties: false, 116 | } as const; 117 | 118 | type ReceivedClosedObject = FromSchema; 119 | type ExpectedClosedObject = { foo: string; bar?: number }; 120 | 121 | type AssertClosedObject = A.Equals; 122 | const assertClosedObject: AssertClosedObject = 1; 123 | assertClosedObject; 124 | 125 | const openObjectSchema = { 126 | type: "object", 127 | unevaluatedProperties: { 128 | type: "boolean", 129 | }, 130 | } as const; 131 | 132 | type ReceivedOpenObject = FromSchema; 133 | type ExpectedOpenObject = { [x: string]: unknown }; 134 | 135 | type AssertOpenObject = A.Equals; 136 | const assertOpenObject: AssertOpenObject = 1; 137 | assertOpenObject; 138 | 139 | // Defaulted property 140 | 141 | const objectWithDefaultedPropertySchema = { 142 | type: "object", 143 | properties: { 144 | foo: { type: "string", default: "bar" }, 145 | }, 146 | additionalProperties: false, 147 | } as const; 148 | 149 | type ReceivedObjectWithDefaultedProperty = FromSchema< 150 | typeof objectWithDefaultedPropertySchema 151 | >; 152 | type ExpectedObjectWithDefaultedProperty = { foo: string }; 153 | type AssertObjectWithDefaultedProperty = A.Equals< 154 | ReceivedObjectWithDefaultedProperty, 155 | ExpectedObjectWithDefaultedProperty 156 | >; 157 | const assertObjectWithDefaultedProperty: AssertObjectWithDefaultedProperty = 1; 158 | assertObjectWithDefaultedProperty; 159 | 160 | type ReceivedObjectWithDefaultedProperty2 = FromSchema< 161 | typeof objectWithDefaultedPropertySchema, 162 | { keepDefaultedPropertiesOptional: true } 163 | >; 164 | type ExpectedObjectWithDefaultedProperty2 = { foo?: string }; 165 | type AssertObjectWithDefaultedProperty2 = A.Equals< 166 | ReceivedObjectWithDefaultedProperty2, 167 | ExpectedObjectWithDefaultedProperty2 168 | >; 169 | const assertObjectWithDefaultedProperty2: AssertObjectWithDefaultedProperty2 = 1; 170 | assertObjectWithDefaultedProperty2; 171 | -------------------------------------------------------------------------------- /src/tests/readme/oneOf.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const catSchema = { 6 | type: "object", 7 | oneOf: [ 8 | { 9 | properties: { 10 | name: { type: "string" }, 11 | }, 12 | required: ["name"], 13 | }, 14 | { 15 | properties: { 16 | color: { enum: ["black", "brown", "white"] }, 17 | }, 18 | }, 19 | ], 20 | } as const; 21 | 22 | type ReceivedCat = FromSchema; 23 | type ExpectedCat = 24 | | { 25 | [x: string]: unknown; 26 | name: string; 27 | } 28 | | { 29 | [x: string]: unknown; 30 | color?: "black" | "brown" | "white"; 31 | }; 32 | 33 | type AssertCat = A.Equals; 34 | const assertCat: AssertCat = 1; 35 | assertCat; 36 | 37 | // Cannot raise error at the moment 38 | const invalidCat: ReceivedCat = { name: "Garfield" }; 39 | invalidCat; 40 | -------------------------------------------------------------------------------- /src/tests/readme/primitive.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const primitiveTypeSchema = { 6 | type: "null", 7 | } as const; 8 | 9 | type ExpectedPrimitive = null; 10 | type ReceivedPrimitive = FromSchema; 11 | 12 | type AssertPrimitive = A.Equals; 13 | const assertPrimitive: AssertPrimitive = 1; 14 | assertPrimitive; 15 | 16 | const primitiveTypesSchema = { 17 | type: ["null", "string"], 18 | } as const; 19 | 20 | type ExpectedPrimitives = null | string; 21 | type ReceivedPrimitives = FromSchema; 22 | 23 | type AssertPrimitives = A.Equals; 24 | const assertPrimitives: AssertPrimitives = 1; 25 | assertPrimitives; 26 | -------------------------------------------------------------------------------- /src/tests/readme/references.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | const userSchema = { 6 | $id: "http://example.com/schemas/user.json", 7 | type: "object", 8 | properties: { 9 | name: { type: "string" }, 10 | age: { type: "integer" }, 11 | }, 12 | required: ["name", "age"], 13 | additionalProperties: false, 14 | } as const; 15 | 16 | const usersSchema = { 17 | type: "array", 18 | items: { 19 | $ref: "http://example.com/schemas/user.json", 20 | }, 21 | } as const; 22 | 23 | type ReceivedUsers = FromSchema< 24 | typeof usersSchema, 25 | { references: [typeof userSchema] } 26 | >; 27 | type ExpectedUsers = { 28 | name: string; 29 | age: number; 30 | }[]; 31 | 32 | type AssertUsers = A.Equals; 33 | const assertUser: AssertUsers = 1; 34 | assertUser; 35 | 36 | const anotherUserSchema = { 37 | $id: "http://example.com/schemas/users.json", 38 | type: "array", 39 | items: { $ref: "user.json" }, 40 | } as const; 41 | 42 | type ReceivedUsers2 = FromSchema< 43 | typeof anotherUserSchema, 44 | { references: [typeof userSchema] } 45 | >; 46 | type ExpectedUsers2 = { 47 | name: string; 48 | age: number; 49 | }[]; 50 | 51 | type AssertUsers2 = A.Equals; 52 | const assertUser2: AssertUsers2 = 1; 53 | assertUser2; 54 | -------------------------------------------------------------------------------- /src/tests/readme/tuple.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | // With additional items 6 | 7 | const tupleWithAdditionalItemsSchema = { 8 | type: "array", 9 | items: [{ type: "boolean" }, { type: "string" }], 10 | } as const; 11 | 12 | type ReceivedTupleWithAdditionalItems = FromSchema< 13 | typeof tupleWithAdditionalItemsSchema 14 | >; 15 | 16 | type ExpectedTupleWithAdditionalItems = 17 | | [] 18 | | [boolean] 19 | | [boolean, string] 20 | | [boolean, string, ...unknown[]]; 21 | 22 | type AssertTupleWithAdditionalItems = A.Equals< 23 | ReceivedTupleWithAdditionalItems, 24 | ExpectedTupleWithAdditionalItems 25 | >; 26 | const assertTupleWithAdditionalItems: AssertTupleWithAdditionalItems = 1; 27 | assertTupleWithAdditionalItems; 28 | 29 | // No additional items 30 | 31 | const tupleWithoutAdditionalItemsSchema = { 32 | type: "array", 33 | items: [{ type: "boolean" }, { type: "string" }], 34 | additionalItems: false, 35 | } as const; 36 | 37 | type ReceivedTupleWithoutAdditionalItems = FromSchema< 38 | typeof tupleWithoutAdditionalItemsSchema 39 | >; 40 | type ExpectedTupleWithoutAdditionalItems = [] | [boolean] | [boolean, string]; 41 | 42 | type AssertTupleWithoutAdditionalItems = A.Equals< 43 | ReceivedTupleWithoutAdditionalItems, 44 | ExpectedTupleWithoutAdditionalItems 45 | >; 46 | const assertTupleWithoutAdditionalItems: AssertTupleWithoutAdditionalItems = 1; 47 | assertTupleWithoutAdditionalItems; 48 | 49 | // Typed additional items 50 | 51 | const tupleWithTypedAdditionalItemsSchema = { 52 | type: "array", 53 | items: [{ type: "boolean" }, { type: "string" }], 54 | additionalItems: { type: "number" }, 55 | } as const; 56 | 57 | type ReceivedTupleWithTypedAdditionalItems = FromSchema< 58 | typeof tupleWithTypedAdditionalItemsSchema 59 | >; 60 | type ExpectedTupleWithTypedAdditionalItems = 61 | | [] 62 | | [boolean] 63 | | [boolean, string] 64 | | [boolean, string, ...number[]]; 65 | 66 | type AssertTupleWithTypedAdditionalItems = A.Equals< 67 | ReceivedTupleWithTypedAdditionalItems, 68 | ExpectedTupleWithTypedAdditionalItems 69 | >; 70 | const assertTupleWithTypedAdditionalItems: AssertTupleWithTypedAdditionalItems = 1; 71 | assertTupleWithTypedAdditionalItems; 72 | 73 | // Min & max items 74 | 75 | const tupleWithMinAndMaxLengthSchema = { 76 | type: "array", 77 | items: [{ type: "boolean" }, { type: "string" }], 78 | minItems: 1, 79 | maxItems: 2, 80 | } as const; 81 | 82 | type ReceivedTupleWithMinAndMaxLength = FromSchema< 83 | typeof tupleWithMinAndMaxLengthSchema 84 | >; 85 | type ExpectedTupleWithMinAndMaxLength = [boolean] | [boolean, string]; 86 | 87 | type AssertTupleWithMinAndMaxLength = A.Equals< 88 | ReceivedTupleWithMinAndMaxLength, 89 | ExpectedTupleWithMinAndMaxLength 90 | >; 91 | const assertTupleWithMinAndMaxLength: AssertTupleWithMinAndMaxLength = 1; 92 | assertTupleWithMinAndMaxLength; 93 | -------------------------------------------------------------------------------- /src/type-utils/and.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return `true` if `A` and `B` extend `true`, `false` otherwise 3 | * @param A Type 4 | * @param B Type 5 | * @returns Boolean 6 | */ 7 | export type And = CONDITION_A extends true 8 | ? CONDITION_B extends true 9 | ? true 10 | : false 11 | : false; 12 | -------------------------------------------------------------------------------- /src/type-utils/extends.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns `true` if type `A` extends type `B`, `false` if not 3 | * @param A Type 4 | * @param B Type 5 | * @returns Boolean 6 | */ 7 | export type DoesExtend = [TYPE_A] extends [TYPE_B] 8 | ? true 9 | : false; 10 | -------------------------------------------------------------------------------- /src/type-utils/get.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the (recursively) nested value of an object for a given path. Returns `DEFAULT` if no value is found. 3 | * @param OBJECT Object 4 | * @param PATH String[] 5 | * @param DEFAULT Type 6 | * @returns Type 7 | */ 8 | export type DeepGet< 9 | OBJECT, 10 | PATH extends string[], 11 | DEFAULT = undefined, 12 | > = PATH extends [infer PATH_HEAD, ...infer PATH_TAIL] 13 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 14 | PATH_HEAD extends string 15 | ? PATH_TAIL extends string[] 16 | ? PATH_HEAD extends keyof OBJECT 17 | ? DeepGet 18 | : DEFAULT 19 | : never 20 | : never 21 | : OBJECT; 22 | -------------------------------------------------------------------------------- /src/type-utils/if.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return `THEN` if `CONDITION` extends `true`, `ELSE` otherwise 3 | * @param CONDITION Boolean 4 | * @param THEN Type 5 | * @param ELSE Type 6 | * @returns Type 7 | */ 8 | export type If< 9 | CONDITION extends boolean, 10 | THEN, 11 | ELSE = never, 12 | > = CONDITION extends true ? THEN : ELSE; 13 | -------------------------------------------------------------------------------- /src/type-utils/index.ts: -------------------------------------------------------------------------------- 1 | export type { And } from "./and"; 2 | export type { DoesExtend } from "./extends"; 3 | export type { DeepGet } from "./get"; 4 | export type { If } from "./if"; 5 | export type { Key } from "./key"; 6 | export type { Join } from "./join"; 7 | export type { Narrow } from "./narrow"; 8 | export type { Not } from "./not"; 9 | export type { Pop } from "./pop"; 10 | export type { Split } from "./split"; 11 | export type { Tail } from "./tail"; 12 | export type { Writable } from "./writable"; 13 | -------------------------------------------------------------------------------- /src/type-utils/join.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Join a tuple of strings together 3 | * @param STRINGS String[] 4 | * @param SEPARATOR String 5 | * @returns String 6 | */ 7 | export type Join< 8 | STRINGS extends string[], 9 | SEPARATOR extends string = ",", 10 | > = STRINGS extends [] 11 | ? "" 12 | : STRINGS extends [string] 13 | ? `${STRINGS[0]}` 14 | : STRINGS extends [string, ...infer STRINGS_TAIL] 15 | ? STRINGS_TAIL extends string[] 16 | ? // TODO increase TS version and use "extends" in Array https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#improved-inference-for-infer-types-in-template-string-types 17 | `${STRINGS[0]}${SEPARATOR}${Join}` 18 | : never 19 | : string; 20 | -------------------------------------------------------------------------------- /src/type-utils/key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Any object key 3 | */ 4 | export type Key = string | number | symbol; 5 | -------------------------------------------------------------------------------- /src/type-utils/narrow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to narrow the inferred generic type of a function 3 | * @param INPUT Type 4 | * @returns Type 5 | */ 6 | export type Narrow = INPUT extends Promise 7 | ? Promise> 8 | : INPUT extends (...args: infer ARGS) => infer RETURN 9 | ? (...args: Narrow) => Narrow 10 | : INPUT extends [] 11 | ? [] 12 | : INPUT extends object 13 | ? { [KEY in keyof INPUT]: Narrow } 14 | : INPUT extends string | number | boolean | bigint 15 | ? INPUT 16 | : never; 17 | -------------------------------------------------------------------------------- /src/type-utils/not.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return `true` if `BOOL` extends `false`, `false` if `BOOL` extends `true`, `never` otherwise 3 | * @param BOOL Boolean 4 | * @returns Boolean 5 | */ 6 | export type Not = BOOL extends false 7 | ? true 8 | : BOOL extends true 9 | ? false 10 | : never; 11 | -------------------------------------------------------------------------------- /src/type-utils/pop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove an item out of a array 3 | * @param ARRAY Array 4 | * @returns Type 5 | */ 6 | export type Pop = ARRAY extends 7 | | readonly [...infer ARRAY_BODY, unknown] 8 | | readonly [...infer ARRAY_BODY, unknown?] 9 | ? ARRAY_BODY 10 | : ARRAY; 11 | -------------------------------------------------------------------------------- /src/type-utils/split.ts: -------------------------------------------------------------------------------- 1 | import type { Pop } from "./pop"; 2 | 3 | /** 4 | * Same as `Split` but doesn't remove the last value in case the SEPARATOR is an empty string 5 | * @param STRING String 6 | * @param SEPARATOR String 7 | * @returns String[] 8 | */ 9 | type RecursiveSplit< 10 | STRING extends string, 11 | SEPARATOR extends string = "", 12 | > = STRING extends `${infer BEFORE}${SEPARATOR}${infer AFTER}` 13 | ? [BEFORE, ...RecursiveSplit] 14 | : [STRING]; 15 | 16 | /** 17 | * Given a string and a separator, split the string into an array of sub-strings delimited by the separator. 18 | * @param STRING String 19 | * @param SEPARATOR String 20 | * @returns String[] 21 | */ 22 | export type Split< 23 | STRING extends string, 24 | SEPARATOR extends string = "", 25 | RESULT extends string[] = RecursiveSplit, 26 | > = SEPARATOR extends "" ? Pop : RESULT; 27 | -------------------------------------------------------------------------------- /src/type-utils/tail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Omit the first element of an array 3 | * @param ARRAY Array 4 | * @returns Array 5 | */ 6 | export type Tail = ARRAY extends readonly [] 7 | ? ARRAY 8 | : ARRAY extends readonly [unknown?, ...infer ARRAY_TAIL] 9 | ? ARRAY_TAIL 10 | : ARRAY; 11 | -------------------------------------------------------------------------------- /src/type-utils/writable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively sets all type properties as writable (non-readonly) 3 | * @param TYPE Type 4 | * @returns Type 5 | */ 6 | export type Writable = TYPE extends 7 | | ((...args: unknown[]) => unknown) 8 | | Date 9 | | RegExp 10 | ? TYPE 11 | : // maps 12 | TYPE extends ReadonlyMap 13 | ? Map, Writable> 14 | : // sets 15 | TYPE extends ReadonlySet 16 | ? Set> 17 | : TYPE extends ReadonlyArray 18 | ? // tuples 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents 20 | `${bigint}` extends `${keyof TYPE & any}` 21 | ? { -readonly [KEY in keyof TYPE]: Writable } 22 | : // arrays 23 | Writable[] 24 | : // objects 25 | TYPE extends object 26 | ? { -readonly [KEY in keyof TYPE]: Writable } 27 | : // primitive or literal value 28 | TYPE; 29 | -------------------------------------------------------------------------------- /src/utils/asConst.ts: -------------------------------------------------------------------------------- 1 | import type { Narrow } from "~/type-utils"; 2 | 3 | /** 4 | * Returns the input parameter without muting it, but narrowing its inferred type. Similar to using the `as const` statement functionnally. 5 | * @param input Input 6 | * @returns Input, narrowly typed 7 | * 8 | * ```ts 9 | * const object = { foo: "bar" } 10 | * // { foo: string } 11 | * 12 | * const narrowedObject = asConst({ foo: "bar "}) 13 | * // => { foo: "bar" } 14 | * ``` 15 | */ 16 | export const asConst = (input: Narrow): Narrow => input; 17 | -------------------------------------------------------------------------------- /src/utils/asConst.type.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { FromSchema } from "~/index"; 4 | 5 | import { asConst } from "./asConst"; 6 | 7 | const number = asConst(1); 8 | const assertNumber: A.Equals = 1; 9 | assertNumber; 10 | 11 | const string = asConst("string"); 12 | const assertString: A.Equals = 1; 13 | assertString; 14 | 15 | const array = asConst([1, "string", true]); 16 | const assertArray: A.Equals = 1; 17 | assertArray; 18 | 19 | const object = asConst({ some: ["object", 1, "string"] }); 20 | const assertObject: A.Equals = 21 | 1; 22 | assertObject; 23 | 24 | const nestedObject = asConst({ 25 | works: { with: { multiple: { nested: ["types"] } } }, 26 | }); 27 | const assertNestedObject: A.Equals< 28 | typeof nestedObject, 29 | { works: { with: { multiple: { nested: ["types"] } } } } 30 | > = 1; 31 | assertNestedObject; 32 | 33 | const func = asConst( 34 | (a: string, b: { some: "object" }): [1, "string", true] => { 35 | a; 36 | b; 37 | 38 | return [1, "string", true]; 39 | }, 40 | ); 41 | const assertFunc: A.Equals< 42 | typeof func, 43 | (a: string, b: { some: "object" }) => [1, "string", true] 44 | > = 1; 45 | assertFunc; 46 | 47 | const promise = asConst(new Promise<1>(resolve => resolve(1))); 48 | const assertPromise: A.Equals> = 1; 49 | assertPromise; 50 | 51 | // On actual schema 52 | 53 | enum AdressType { 54 | residential = "residential", 55 | business = "business", 56 | } 57 | 58 | const addressSchema = asConst({ 59 | type: "object", 60 | allOf: [ 61 | { 62 | properties: { 63 | address: { type: "string" }, 64 | city: { type: "string" }, 65 | state: { type: "string" }, 66 | }, 67 | required: ["address", "city", "state"], 68 | }, 69 | { 70 | properties: { 71 | type: { enum: Object.values(AdressType) }, 72 | }, 73 | }, 74 | ], 75 | }); 76 | 77 | type ReceivedAddress = FromSchema; 78 | type ExpectedAddress = { 79 | [x: string]: unknown; 80 | address: string; 81 | city: string; 82 | state: string; 83 | type?: AdressType; 84 | }; 85 | 86 | type AssertAddress = A.Equals; 87 | const assertAddress: AssertAddress = 1; 88 | assertAddress; 89 | -------------------------------------------------------------------------------- /src/utils/asConst.unit.test.ts: -------------------------------------------------------------------------------- 1 | import { asConst } from "./asConst"; 2 | 3 | describe("asConst", () => { 4 | it("returns argument without modifying it", () => { 5 | expect(asConst(1)).toStrictEqual(1); 6 | 7 | expect(asConst({ some: "object" })).toStrictEqual({ some: "object" }); 8 | 9 | expect(asConst(["some", { complex: "array" }])).toStrictEqual([ 10 | "some", 11 | { complex: "array" }, 12 | ]); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export type { $Compiler, Compiler, $Validator, Validator } from "./type-guards"; 2 | export { 3 | wrapCompilerAsTypeGuard, 4 | wrapValidatorAsTypeGuard, 5 | } from "./type-guards"; 6 | export { asConst } from "./asConst"; 7 | -------------------------------------------------------------------------------- /src/utils/type-guards/ajv.util.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | 3 | export const ajv = new Ajv(); 4 | -------------------------------------------------------------------------------- /src/utils/type-guards/compiler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FromSchema, 3 | FromSchemaDefaultOptions, 4 | FromSchemaOptions, 5 | JSONSchema, 6 | } from "~/index"; 7 | 8 | /** 9 | * Any compiler function type (non type-guarding) 10 | */ 11 | export type $Compiler = ( 12 | schema: JSONSchema, 13 | ...compilingOptions: C 14 | ) => (data: unknown, ...validationOptions: V) => boolean; 15 | 16 | /** 17 | * Adds type guarding to a validator function 18 | * 19 | * ```ts 20 | * const compiler: Compiler = >( 21 | * schema: S, 22 | * ) => (data: unknown): data is T => { 23 | * const isDataValid: boolean = ... // Implement validation here 24 | * return isDataValid; 25 | * }; 26 | * ``` 27 | */ 28 | export type Compiler< 29 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 30 | C extends unknown[] = [], 31 | V extends unknown[] = [], 32 | > = >( 33 | schema: S, 34 | ...compilingOptions: C 35 | ) => (data: unknown, ...validationOptions: V) => data is T; 36 | 37 | /** 38 | * Type definition for `wrapCompilerAsTypeGuard` 39 | */ 40 | type CompilerWrapper = < 41 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 42 | C extends unknown[] = [], 43 | V extends unknown[] = [], 44 | >( 45 | compiler: $Compiler, 46 | ) => Compiler; 47 | 48 | /** 49 | * Adds type guarding to any compiler function (doesn't modify it) 50 | * @param compiler Compiler function 51 | * @returns Compiler function with type guarding 52 | */ 53 | export const wrapCompilerAsTypeGuard: CompilerWrapper = 54 | < 55 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 56 | C extends unknown[] = [], 57 | V extends unknown[] = [], 58 | >( 59 | compiler: $Compiler, 60 | ) => 61 | >( 62 | schema: S, 63 | ...compilingOptions: C 64 | ) => { 65 | const validator = compiler(schema, ...compilingOptions); 66 | 67 | return (data: unknown, ...validationOptions: V): data is T => 68 | validator(data, ...validationOptions); 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/type-guards/compiler.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { JSONSchema } from "~/index"; 4 | 5 | import { ajv } from "./ajv.util.test"; 6 | import { $Compiler, wrapCompilerAsTypeGuard } from "./compiler"; 7 | import { Pet, petSchema } from "./schema.util.test"; 8 | 9 | const $compile: $Compiler = schema => ajv.compile(schema); 10 | const compile = wrapCompilerAsTypeGuard($compile); 11 | const isPet = compile(petSchema); 12 | 13 | type CompilingOtions = [{ fastCompile: boolean }]; 14 | type ValidationOptions = [{ shouldThrow: boolean }]; 15 | const $compileWithOptions: $Compiler = ( 16 | schema, 17 | compilingOptions, 18 | ) => { 19 | const { fastCompile } = compilingOptions; 20 | 21 | const validator = ajv.compile(schema, fastCompile); 22 | 23 | return (data, validationOptions) => { 24 | const { shouldThrow } = validationOptions; 25 | const isValid = validator(data); 26 | 27 | if (isValid) return true; 28 | 29 | if (shouldThrow) { 30 | throw new Error(); 31 | } 32 | 33 | return false; 34 | }; 35 | }; 36 | const compileWithOptions = wrapCompilerAsTypeGuard($compileWithOptions); 37 | const isPetWithOptions = compileWithOptions(petSchema, { fastCompile: true }); 38 | 39 | describe("Compiler", () => { 40 | it("accepts valid data", () => { 41 | const validData: unknown = { name: "Dogo", age: 13 }; 42 | expect(isPet(validData)).toBe(true); 43 | }); 44 | 45 | it("rejects invalid data", () => { 46 | const invalidData: unknown = { name: 13, age: "13" }; 47 | 48 | const assertInvalidPet: A.Equals = 1; 49 | assertInvalidPet; 50 | 51 | if (isPet(invalidData)) { 52 | const assertValidPet: A.Equals = 1; 53 | assertValidPet; 54 | } 55 | 56 | expect(isPet(invalidData)).toBe(false); 57 | }); 58 | 59 | it("accepts valid data (with validationOptions)", () => { 60 | const validData: unknown = { name: "Dogo", age: 13 }; 61 | 62 | const assertCompilerOptions: A.Equals< 63 | Parameters, 64 | [JSONSchema, ...CompilingOtions] 65 | > = 1; 66 | assertCompilerOptions; 67 | 68 | const assertValidatorOptions: A.Equals< 69 | Parameters, 70 | [unknown, ...ValidationOptions] 71 | > = 1; 72 | assertValidatorOptions; 73 | 74 | expect(isPetWithOptions(validData, { shouldThrow: true })).toBe(true); 75 | }); 76 | 77 | it("rejects invalid data (with validationOptions)", () => { 78 | const invalidData: unknown = { name: 13, age: "13" }; 79 | const validationOptions = { shouldThrow: false }; 80 | 81 | const assertInvalidPet: A.Equals = 1; 82 | assertInvalidPet; 83 | 84 | if (isPetWithOptions(invalidData, validationOptions)) { 85 | const assertValidPet: A.Equals = 1; 86 | assertValidPet; 87 | } 88 | 89 | expect(isPetWithOptions(invalidData, validationOptions)).toBe(false); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/utils/type-guards/index.ts: -------------------------------------------------------------------------------- 1 | export type { $Compiler, Compiler } from "./compiler"; 2 | export { wrapCompilerAsTypeGuard } from "./compiler"; 3 | export type { $Validator, Validator } from "./validator"; 4 | export { wrapValidatorAsTypeGuard } from "./validator"; 5 | -------------------------------------------------------------------------------- /src/utils/type-guards/schema.util.test.ts: -------------------------------------------------------------------------------- 1 | import type { FromSchema } from "~/index"; 2 | 3 | export const petSchema = { 4 | type: "object", 5 | properties: { 6 | age: { type: "integer" }, 7 | name: { type: "string" }, 8 | }, 9 | required: ["age", "name"], 10 | additionalProperties: false, 11 | } as const; 12 | 13 | export type Pet = FromSchema; 14 | -------------------------------------------------------------------------------- /src/utils/type-guards/validator.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FromSchema, 3 | FromSchemaDefaultOptions, 4 | FromSchemaOptions, 5 | JSONSchema, 6 | } from "~/index"; 7 | 8 | /** 9 | * Any validator function type (non type-guarding) 10 | */ 11 | export type $Validator = ( 12 | schema: JSONSchema, 13 | data: unknown, 14 | ...validationOptions: V 15 | ) => boolean; 16 | 17 | /** 18 | * Adds type guarding to a validator function 19 | * 20 | * ```ts 21 | * const validate: Validator = >( 22 | * schema: S, 23 | * data: unknown 24 | * ): data is T => { 25 | * const isDataValid: boolean = ... // Implement validation here 26 | * return isDataValid; 27 | * }; 28 | * ``` 29 | */ 30 | export type Validator< 31 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 32 | V extends unknown[] = [], 33 | > = >( 34 | schema: S, 35 | data: unknown, 36 | ...validationOptions: V 37 | ) => data is T; 38 | 39 | /** 40 | * Type definition for wrapValidatorAsTypeGuard 41 | */ 42 | type ValidatorWrapper = < 43 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 44 | V extends unknown[] = [], 45 | >( 46 | validator: $Validator, 47 | ) => Validator; 48 | 49 | /** 50 | * Adds type guarding to any validator function (doesn't modify it) 51 | * @param validator Validator function 52 | * @returns Validator function with type guarding 53 | */ 54 | export const wrapValidatorAsTypeGuard: ValidatorWrapper = 55 | < 56 | O extends FromSchemaOptions = FromSchemaDefaultOptions, 57 | V extends unknown[] = [], 58 | >( 59 | validator: $Validator, 60 | ) => 61 | >( 62 | schema: S, 63 | data: unknown, 64 | ...validationOptions: V 65 | ): data is T => 66 | validator(schema, data, ...validationOptions); 67 | -------------------------------------------------------------------------------- /src/utils/type-guards/validator.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { A } from "ts-toolbelt"; 2 | 3 | import type { JSONSchema } from "~/index"; 4 | 5 | import { ajv } from "./ajv.util.test"; 6 | import { Pet, petSchema } from "./schema.util.test"; 7 | import { $Validator, wrapValidatorAsTypeGuard } from "./validator"; 8 | 9 | const $validate: $Validator = (schema, data) => ajv.validate(schema, data); 10 | const validate = wrapValidatorAsTypeGuard($validate); 11 | 12 | type ValidationOptions = [{ shouldThrow: boolean }]; 13 | const $validateWithOptions: $Validator = ( 14 | schema, 15 | data, 16 | validationOptions, 17 | ) => { 18 | const { shouldThrow } = validationOptions; 19 | const isValid = ajv.validate(schema, data); 20 | 21 | if (isValid) return true; 22 | 23 | if (shouldThrow) { 24 | throw new Error(); 25 | } 26 | 27 | return false; 28 | }; 29 | const validateWithOptions = wrapValidatorAsTypeGuard($validateWithOptions); 30 | 31 | describe("Validator", () => { 32 | it("accepts valid data", () => { 33 | const validData: unknown = { name: "Dogo", age: 13 }; 34 | expect(validate(petSchema, validData)).toBe(true); 35 | }); 36 | 37 | it("rejects invalid data", () => { 38 | const invalidData: unknown = { name: 13, age: "13" }; 39 | 40 | const assertInvalidPet: A.Equals = 1; 41 | assertInvalidPet; 42 | 43 | if (validate(petSchema, invalidData)) { 44 | const assertValidPet: A.Equals = 1; 45 | assertValidPet; 46 | } 47 | 48 | expect(validate(petSchema, invalidData)).toBe(false); 49 | }); 50 | 51 | it("accepts valid data (with validationOptions)", () => { 52 | const validData: unknown = { name: "Dogo", age: 13 }; 53 | 54 | const assertOptions: A.Equals< 55 | Parameters, 56 | [JSONSchema, unknown, ...ValidationOptions] 57 | > = 1; 58 | assertOptions; 59 | 60 | expect( 61 | validateWithOptions(petSchema, validData, { shouldThrow: true }), 62 | ).toBe(true); 63 | }); 64 | 65 | it("rejects invalid data (with validationOptions)", () => { 66 | const invalidData: unknown = { name: 13, age: "13" }; 67 | const validationOptions = { shouldThrow: false }; 68 | 69 | const assertInvalidPet: A.Equals = 1; 70 | assertInvalidPet; 71 | 72 | if (validateWithOptions(petSchema, invalidData, validationOptions)) { 73 | const assertValidPet: A.Equals = 1; 74 | assertValidPet; 75 | } 76 | 77 | expect(validateWithOptions(petSchema, invalidData, validationOptions)).toBe( 78 | false, 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "exclude": ["./lib", "./builds", "src/tests", "./**/*.test.ts", "./scripts"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["es2017"], 5 | "baseUrl": "src", 6 | "outDir": "lib/types", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "removeComments": true, 15 | "resolveJsonModule": true, 16 | "paths": { 17 | "~/*": ["*"] 18 | }, 19 | "plugins": [{ "transform": "@zerollup/ts-transform-paths" }] 20 | }, 21 | "exclude": ["builds", "lib"] 22 | } 23 | --------------------------------------------------------------------------------