├── .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:
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 | 
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