Repository: jquense/yup Branch: master Commit: ff31eee8a2b1 Files: 73 Total size: 445.6 KB Directory structure: gitextract_was633h2/ ├── .babelrc.js ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── question.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .yarn/ │ └── patches/ │ └── @4c-rollout-npm-4.0.2-ab2b6d0bab.patch ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs/ │ └── extending.md ├── package.json ├── renovate.json ├── rollup.config.js ├── src/ │ ├── Condition.ts │ ├── Lazy.ts │ ├── Reference.ts │ ├── ValidationError.ts │ ├── array.ts │ ├── boolean.ts │ ├── date.ts │ ├── globals.d.ts │ ├── index.ts │ ├── locale.ts │ ├── mixed.ts │ ├── number.ts │ ├── object.ts │ ├── schema.ts │ ├── setLocale.ts │ ├── standardSchema.ts │ ├── string.ts │ ├── tuple.ts │ ├── types.ts │ └── util/ │ ├── ReferenceSet.ts │ ├── cloneDeep.ts │ ├── createValidation.ts │ ├── isAbsent.ts │ ├── isSchema.ts │ ├── objectTypes.ts │ ├── parseIsoDate.ts │ ├── parseJson.ts │ ├── printValue.ts │ ├── reach.ts │ ├── sortByKeyOrder.ts │ ├── sortFields.ts │ ├── toArray.ts │ └── types.ts ├── test/ │ ├── .eslintignore │ ├── ValidationError.ts │ ├── array.ts │ ├── bool.ts │ ├── date.ts │ ├── helpers.ts │ ├── lazy.ts │ ├── mixed.ts │ ├── number.ts │ ├── object.ts │ ├── setLocale.ts │ ├── standardSchema.ts │ ├── string.ts │ ├── tsconfig.json │ ├── tuple.ts │ ├── types/ │ │ ├── .eslintrc.js │ │ └── types.ts │ ├── util/ │ │ └── parseIsoDate.ts │ └── yup.js ├── test-setup.mjs ├── tsconfig.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ module.exports = (api) => ({ presets: [ [ 'babel-preset-env-modules', api.env() !== 'test' ? { ignoreBrowserslistConfig: true, modules: api.env() === 'esm' ? false : 'commonjs', } : { target: 'node', targets: { node: 'current' }, }, ], ['@babel/preset-typescript', { allowDeclareFields: true }], ], }); ================================================ FILE: .eslintignore ================================================ .eslintrc .eslintrc.js ================================================ FILE: .eslintrc ================================================ { "extends": ["jason", "prettier"], "env": { "browser": true }, "parserOptions": { "requireConfigFile": false }, "rules": { "@typescript-eslint/no-shadow": "off", "@typescript-eslint/no-empty-interface": "off" }, "overrides": [ { "files": ["test/**"], "plugins": ["jest"], "env": { "jest/globals": true }, "rules": { "global-require": "off", "no-await-in-loop": "off", "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/prefer-to-have-length": "warn", "@typescript-eslint/no-empty-function": "off" } } ] } ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp *.sln merge=union *.csproj merge=union *.vbproj merge=union *.fsproj merge=union *.dbproj merge=union # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Write a **runnable** test case using the code sandbox template: https://codesandbox.io/s/yup-test-case-gg1g1 In the `index.test.js` file change the passing test to a failing one demostrating your issue > NOTE: if you do not provide a runnable reproduction the chances of getting feedback are significantly lower **Expected behavior** A clear and concise description of what you expected to happen. **Platform (please complete the following information):** - Browser [e.g. chrome, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: General questions about yup or how it works title: '' labels: '' assignees: '' --- - Write a title that summarizes the specific problem - Describe what you are trying to accomplish AND what you have tried **Help Others Reproduce** Write a **runnable** test case using the code sandbox template: https://codesandbox.io/s/yup-test-case-gg1g1 > NOTE: if you do not provide a runnable reproduction the chances of getting feedback are significantly lower ================================================ FILE: .github/workflows/ci.yml ================================================ name: Test on: push: branches: [master, next] pull_request: branches: [master, next] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: 'lts/*' - run: | corepack enable - run: yarn install --frozen-lockfile - run: yarn test ================================================ FILE: .gitignore ================================================ # Logs logs *.log dts/ # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules # Users Environment Variables .lock-wscript # ========================= # Operating System Files # ========================= # OSX # ========================= .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Windows # ========================= # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Ignore build files lib/ es/ ================================================ FILE: .nvmrc ================================================ 22.19.0 ================================================ FILE: .yarn/patches/@4c-rollout-npm-4.0.2-ab2b6d0bab.patch ================================================ diff --git a/command.js b/command.js index 9608bffb4a52e5b8066e263d1286420ae92988bb..86ca58d70f315dae45fc739707dcff07a99a2be4 100644 --- a/command.js +++ b/command.js @@ -292,8 +292,7 @@ const handlerImpl = async (argv) => { task: () => exec('yarn', [ 'install', - '--frozen-lockfile', - '--production=false', + '--immutable', ]), } : { ================================================ FILE: .yarnrc.yml ================================================ nodeLinker: node-modules ================================================ FILE: CHANGELOG.md ================================================ ## [1.7.1](https://github.com/jquense/yup/compare/v1.7.0...v1.7.1) (2025-09-21) # [1.7.0](https://github.com/jquense/yup/compare/v1.6.1...v1.7.0) (2025-08-01) ### Features * Implement standard schema interface ([#2258](https://github.com/jquense/yup/issues/2258)) ([ced5f51](https://github.com/jquense/yup/commit/ced5f514a6033a96f5de3b4ae9c17fe0de86d68f)) * resolve ref params if present when describing ([ef53030](https://github.com/jquense/yup/commit/ef5303025c38e6e0dc0de53c990e7277cc74164e)), closes [#2276](https://github.com/jquense/yup/issues/2276) ## [1.6.1](https://github.com/jquense/yup/compare/v1.6.0...v1.6.1) (2024-12-17) ### Bug Fixes * lazy validation errors thrown in builders should resolve async like other validations ([c7d7f97](https://github.com/jquense/yup/commit/c7d7f977e02a7e578950dff192057e0b200999bd)) # [1.6.0](https://github.com/jquense/yup/compare/v1.5.0...v1.6.0) (2024-12-16) ### Features * expose LazySchema ([2b0f126](https://github.com/jquense/yup/commit/2b0f1264083fccb646f7f6bd43adfbff2caaf295)) # [1.5.0](https://github.com/jquense/yup/compare/v1.4.0...v1.5.0) (2024-12-03) ### Bug Fixes * **readme:** some typos and update CustomizingErrors doc ([#2163](https://github.com/jquense/yup/issues/2163)) ([5c77e0d](https://github.com/jquense/yup/commit/5c77e0d4f9373151bcf0cd558c95986b6e4800d7)) ### Features * Add exact and stripUnknown method to object() ([adcdd8d](https://github.com/jquense/yup/commit/adcdd8dd500c627b1efbe3595b6b37dec2847ad8)) # [1.4.0](https://github.com/jquense/yup/compare/v1.3.3...v1.4.0) (2024-03-06) ### Bug Fixes * add optional message to nonNullable schema methods ([#2119](https://github.com/jquense/yup/issues/2119)) ([9e1df49](https://github.com/jquense/yup/commit/9e1df4938c1964a21e6ece0c458bb96dc5aff108)) ### Features * **string:** Create .datetime() ([#2087](https://github.com/jquense/yup/issues/2087)) ([2a9e060](https://github.com/jquense/yup/commit/2a9e060594423018f517419ef5d2f10e417c9fbd)) ## [1.3.3](https://github.com/jquense/yup/compare/v1.3.2...v1.3.3) (2023-12-14) ### Bug Fixes * **addMethod:** allow Schema without making TypeScript upset ([f921fe6](https://github.com/jquense/yup/commit/f921fe69a2d6ecc6e7d0101d2bd81148dfe83e64)) ## [1.3.2](https://github.com/jquense/yup/compare/v1.3.1...v1.3.2) (2023-09-29) ### Bug Fixes * pick and omit with excluded edges ([6956ee7](https://github.com/jquense/yup/commit/6956ee788369dff00e5ecadb506726af3598a87e)), closes [#2097](https://github.com/jquense/yup/issues/2097) ## [1.3.1](https://github.com/jquense/yup/compare/v1.3.0...v1.3.1) (2023-09-26) ### Bug Fixes * ValidationError extends Error ([bc5121b](https://github.com/jquense/yup/commit/bc5121b92d8e16baf8fe9b83f0247a4e90e169b8)) # [1.3.0](https://github.com/jquense/yup/compare/v1.2.0...v1.3.0) (2023-09-23) ### Bug Fixes * add tuple to locale object ([#2100](https://github.com/jquense/yup/issues/2100)) ([809b55a](https://github.com/jquense/yup/commit/809b55a9c16e0cd567f4eced9b9ab02ad8b0bffa)) * performance improvement ([#2043](https://github.com/jquense/yup/issues/2043)) ([#2044](https://github.com/jquense/yup/issues/2044)) ([ee1b731](https://github.com/jquense/yup/commit/ee1b7317b0a9fc0e16a7d33064c3e5584bd7f2d5)) ### Features * Allow schema metadata to be strongly typed ([#2021](https://github.com/jquense/yup/issues/2021)) ([e593f8f](https://github.com/jquense/yup/commit/e593f8f72e7195cf0ac48fa8e1cd82d95c1e6bb5)) ### Reverts * Revert "fix: performance improvement (#2043) (#2044)" (#2071) ([b940eef](https://github.com/jquense/yup/commit/b940eef48eb7456622ae384d0ffa7363d4fbad25)), closes [#2043](https://github.com/jquense/yup/issues/2043) [#2044](https://github.com/jquense/yup/issues/2044) [#2071](https://github.com/jquense/yup/issues/2071) # [1.2.0](https://github.com/jquense/yup/compare/v1.1.1...v1.2.0) (2023-05-25) ### Features * expose printValue ([#2002](https://github.com/jquense/yup/issues/2002)) ([#2008](https://github.com/jquense/yup/issues/2008)) ([1fadba1](https://github.com/jquense/yup/commit/1fadba10b0d1cad60f3708bd28282ab04a55eff6)) * pass options to `default(options => value)` ([e5c5440](https://github.com/jquense/yup/commit/e5c5440767d32a8be6c4a12a5f6176924e058fd2)), closes [#1984](https://github.com/jquense/yup/issues/1984) ## [1.1.1](https://github.com/jquense/yup/compare/v1.1.0...v1.1.1) (2023-04-14) ### Bug Fixes * **docs:** Broken anchores ([#1979](https://github.com/jquense/yup/issues/1979)) ([4ed4576](https://github.com/jquense/yup/commit/4ed45762e955ac6af0dec935a91e815a5a1cf5b9)) * make null validation errors consistent across schema ([#1982](https://github.com/jquense/yup/issues/1982)) ([f999497](https://github.com/jquense/yup/commit/f99949747456d7bf55da3dd38dcf86bbddba3169)) * **object:** excluded edges are merged when concating schema ([c07b08f](https://github.com/jquense/yup/commit/c07b08f033be8eea00d74a5da1cf735cf97e69df)), closes [#1969](https://github.com/jquense/yup/issues/1969) # [1.1.0](https://github.com/jquense/yup/compare/v1.0.2...v1.1.0) (2023-04-12) ### Bug Fixes * tuple describe() method ([#1947](https://github.com/jquense/yup/issues/1947)) ([297f168](https://github.com/jquense/yup/commit/297f1682296ee0b53e5e252477d5a6d7d82df707)) ### Features * only resolve "strip()" for schema when used as an object field ([#1977](https://github.com/jquense/yup/issues/1977)) ([2ba1104](https://github.com/jquense/yup/commit/2ba1104798dcf3b9385997e5fbaa41b4d711472d)) * respect context for object's children ([#1971](https://github.com/jquense/yup/issues/1971)) ([edfe6ac](https://github.com/jquense/yup/commit/edfe6acde9e11ec2bfe2ad41aad867daae7041ce)) ## [1.0.2](https://github.com/jquense/yup/compare/v1.0.0...v1.0.2) (2023-02-27) ### Bug Fixes * fix array describe not including conditions ([4040592](https://github.com/jquense/yup/commit/4040592ccd068ab71e06417b4d355007636cb78c)), closes [#1920](https://github.com/jquense/yup/issues/1920) ## [1.0.1](https://github.com/jquense/yup/compare/v1.0.0...v1.0.1) (2023-02-25) # [1.0.0](https://github.com/jquense/yup/compare/v1.0.0-beta.8...v1.0.0) (2023-02-08) ### Migrating from 0.x to 1.0.0: [#1906](https://github.com/jquense/yup/issues/1906) # [1.0.0-beta.8](https://github.com/jquense/yup/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-11-10) ### Bug Fixes * check if field exists when generating defaults ([37f686c](https://github.com/jquense/yup/commit/37f686c217a9ee5d6f21f07a812a20467ee83578)) * correct minor typo in README ([#1760](https://github.com/jquense/yup/issues/1760)) ([62786c4](https://github.com/jquense/yup/commit/62786c42ca07a2b84b05ca8c473bc01f0c868a94)) * don't return any for oneOf ([74c5bc5](https://github.com/jquense/yup/commit/74c5bc54220cae5ff491ed92845ecd9c1ed7fbf3)), closes [#1675](https://github.com/jquense/yup/issues/1675) * export more types ([f250109](https://github.com/jquense/yup/commit/f250109dbf7158f1ee31ccd11f8309d660880252)) * string().notRequired() ([#1824](https://github.com/jquense/yup/issues/1824)) ([dcb4b63](https://github.com/jquense/yup/commit/dcb4b6381eac21f8f28297066e71920a788c8a47)) * TS 4.8 compat ([bc74c34](https://github.com/jquense/yup/commit/bc74c340721da2ea6e65cb27b967c2970af44d35)) * **types:** undefined defaults produce optional outputs ([1afbac0](https://github.com/jquense/yup/commit/1afbac06edfd3277a8c76bb4c8874cf16d4d346d)) ### Features * add some more type exports ([d5e9c99](https://github.com/jquense/yup/commit/d5e9c99e6ef068bff4c4f92db5ccc0835f6b84b3)) * Export ValidateOptions, ISchema for external use ([#1812](https://github.com/jquense/yup/issues/1812)) ([584df11](https://github.com/jquense/yup/commit/584df11b60e5d47876946872973764d0e0e0c9ed)) * respect nullable() with oneOf ([#1757](https://github.com/jquense/yup/issues/1757)) ([61ec302](https://github.com/jquense/yup/commit/61ec3027caba72cb795ee64f571ca0a7aa6bc9a6)), closes [#768](https://github.com/jquense/yup/issues/768) [#104](https://github.com/jquense/yup/issues/104) * simplify email validation ([440db3e](https://github.com/jquense/yup/commit/440db3e6177d25c06be76995a1deff6e25a90c10)) ### BREAKING CHANGES * previously `oneOf` required adding `null` explicitly to allowed values when using oneOf. Folks have found this confusing and unintuitive so I am deferring and adjusting the behavior * Use a simpler regex for email addresses that aligns with browsers, and add docs about how to override. # [1.0.0-beta.7](https://github.com/jquense/yup/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-08-20) # [1.0.0-beta.6](https://github.com/jquense/yup/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-08-20) ### Bug Fixes * change mixed generic to unknown from any ([5e8e8ef](https://github.com/jquense/yup/commit/5e8e8ef132574b31056bc7c504b8ba62c9ae4d1e)) * count stripping unknown fields as changes for object casts ([2b4773c](https://github.com/jquense/yup/commit/2b4773ca8d4dc7b1f30e3927a113eb807d254f37)), closes [#1620](https://github.com/jquense/yup/issues/1620) * **types:** export more types ([433a452](https://github.com/jquense/yup/commit/433a45252cac4621c00adbeb3c9320caca55cced)) # [1.0.0-beta.5](https://github.com/jquense/yup/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-08-19) ### Bug Fixes * coarce -> coerce ([#1677](https://github.com/jquense/yup/issues/1677)) ([99aa257](https://github.com/jquense/yup/commit/99aa25787a8ff15fe42e54db88ec3ed547357302)) * **docs:** correct typo "coarce" to "coerce" ([#1654](https://github.com/jquense/yup/issues/1654)) ([f29ff71](https://github.com/jquense/yup/commit/f29ff71e4ae04927d85a00a993a014de652ae9fe)) ### Features * add cast nullability migration path. ([#1749](https://github.com/jquense/yup/issues/1749)) ([2bb099e](https://github.com/jquense/yup/commit/2bb099e26f62dd4734fe7bd525d011ce2b1583b5)) * better Lazy types and deepPartial fixes ([#1748](https://github.com/jquense/yup/issues/1748)) ([e4ae6ed](https://github.com/jquense/yup/commit/e4ae6edeb171f25c43ca9367038ad5f09ce9de7c)) ### BREAKING CHANGES * The types for Lazy have changes a bit, it's unlikely that this affects anyone but it is technically a breaking change. # [1.0.0-beta.4](https://github.com/jquense/yup/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-04-10) ### Bug Fixes * **boolean:** calling optional made it non-optional ([4ba02a1](https://github.com/jquense/yup/commit/4ba02a15b649dccaa090a2e72476c1ea448a3fc1)), closes [#1627](https://github.com/jquense/yup/issues/1627) # [1.0.0-beta.3](https://github.com/jquense/yup/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-03-09) ### Bug Fixes * correct minor typo in README ([#1582](https://github.com/jquense/yup/issues/1582)) ([facea53](https://github.com/jquense/yup/commit/facea53e3508d041d86076ef065fb80b8ec74286)) * partial() ([1207261](https://github.com/jquense/yup/commit/120726175aa97a9066fb765155ae4fef15b1e0ad)) ### BREAKING CHANGES * 'required' no longer adds a test for most schema, to determine if a schema is required, check it's `spec.optional` and `spec.nullable` values, also accessible via `describe()` # [1.0.0-beta.2](https://github.com/jquense/yup/compare/v0.32.11...v1.0.0-beta.2) (2022-01-21) ### Bug Fixes * add originalValue to TestContext type ([#1527](https://github.com/jquense/yup/issues/1527)) ([fcc5ae7](https://github.com/jquense/yup/commit/fcc5ae710a1b3ef4b799532291faf894bdbcc11b)), closes [/github.com/abnersajr/DefinitelyTyped/blob/a186d99d0c3a92424691a82130374a1b9145c7cd/types/yup/index.d.ts#L446](https://github.com//github.com/abnersajr/DefinitelyTyped/blob/a186d99d0c3a92424691a82130374a1b9145c7cd/types/yup/index.d.ts/issues/L446) * Merge next into master (#1547) ([366f7d8](https://github.com/jquense/yup/commit/366f7d8e280b021bbd7a4a4d3cfc8fa0cce00c8b)), closes [#1547](https://github.com/jquense/yup/issues/1547) [#1542](https://github.com/jquense/yup/issues/1542) [#1541](https://github.com/jquense/yup/issues/1541) [#1543](https://github.com/jquense/yup/issues/1543) [#1545](https://github.com/jquense/yup/issues/1545) ### Features * add Tuple type ([#1546](https://github.com/jquense/yup/issues/1546)) ([a8febdd](https://github.com/jquense/yup/commit/a8febddcfbe42358e63194ae8da582e66b746edf)) ### BREAKING CHANGES * The builder object version of `when()` requires `then` and `otherwise` to be functions `(schema: Schema) => Schema`. * The function version of `when()` has been changed to make it easier to type. values are always passed as an array and schema, and options always the second and third argument. `this` is no longer set to the schema instance. and all functions _must_ return a schema to be type safe ```diff string() - .when('other', function (other) => { - if (other) return this.required() + .when('other', ([other], schema) => { + return other ? schema.required() : schema }) ``` * concat works shallowly now. Previously concat functioned like a deep merge for object, which produced confusing behavior with incompatible concat'ed schema. Now concat for objects works similar to how it works for other types, the provided schema is applied on top of the existing schema, producing a new schema that is the same as calling each builder method in order * docs: update readme * chore: update to readonly arrays and test string type narrowing * test: add boolean tests * docs: more docs * feat: allow mixed schema to specify type check * `mixed` schema are no longer treated as the base class for other schema types. It hasn't been for a while, but we've done some nasty prototype slinging to make it behave like it was. Now typescript types should be 1 to 1 with the actual classes yup exposes. In general this should not affect anything unless you are extending (via `addMethod` or otherwise) `mixed` prototype. ```diff import { - mixed, + Schema, } from 'yup'; - addMethod(mixed, 'method', impl) + addMethod(Schema, 'method', impl) ``` * chore: prep work for toggling coercion * Publish v1.0.0-alpha.4 * chore: docs * feat!: add json() method and remove default object/array coercion * object and array schema no longer parse JSON strings by default, nor do they return `null` for invalid casts. ```ts object().json().cast('{}') array().json().cast('[]') ``` to mimic the previous behavior * feat: Make Array generic consistent with others * types only, `ArraySchema` initial generic is the array type not the type of the array element. `array()` is still the inner type. * Publish v1.0.0-beta.0 * docs # [1.0.0-beta.1](https://github.com/jquense/yup/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2022-01-03) ### Features * flat bundles and size reductions ([753abdf](https://github.com/jquense/yup/commit/753abdf329e33e43c334e405baa9c71999079480)) # [1.0.0-beta.0](https://github.com/jquense/yup/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2021-12-29) * feat!: add json() method and remove default object/array coercion ([94b73c4](https://github.com/jquense/yup/commit/94b73c438b3d355253f488325e06c69378e71fc1)) ### Features * Make Array generic consistent with others ([a82353f](https://github.com/jquense/yup/commit/a82353f37735daec6e42d18bd4cc0efe52a20f50)) ### BREAKING CHANGES * types only, `ArraySchema` initial generic is the array type not the type of the array element. `array()` is still the inner type. * object and array schema no longer parse JSON strings by default, nor do they return `null` for invalid casts. ```ts object().json().cast('{}') array().json().cast('[]') ``` to mimic the previous behavior # [1.0.0-alpha.4](https://github.com/jquense/yup/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2021-12-29) ### Bug Fixes - add originalValue to TestContext type ([#1527](https://github.com/jquense/yup/issues/1527)) ([fcc5ae7](https://github.com/jquense/yup/commit/fcc5ae710a1b3ef4b799532291faf894bdbcc11b)), closes [/github.com/abnersajr/DefinitelyTyped/blob/a186d99d0c3a92424691a82130374a1b9145c7cd/types/yup/index.d.ts#L446](https://github.com//github.com/abnersajr/DefinitelyTyped/blob/a186d99d0c3a92424691a82130374a1b9145c7cd/types/yup/index.d.ts/issues/L446) ### Features - allow mixed schema to specify type check ([3923039](https://github.com/jquense/yup/commit/3923039558733d34586df2b282d34c5b6cbc5111)) - concat() is shallow and does not merge ([#1541](https://github.com/jquense/yup/issues/1541)) ([a2f99d9](https://github.com/jquense/yup/commit/a2f99d9e8d8ba1b285fa6f48a0dd77a77f629ee4)) - simplify base class hierarchy ([#1543](https://github.com/jquense/yup/issues/1543)) ([c184dcf](https://github.com/jquense/yup/commit/c184dcf644c09f3c4697cd3e5c795784a5315f77)) - stricter `when` types and API ([#1542](https://github.com/jquense/yup/issues/1542)) ([da74254](https://github.com/jquense/yup/commit/da742545a228b909fef6f7fa526ea7b459d96051)) ### BREAKING CHANGES - `mixed` schema are no longer treated as the base class for other schema types. It hasn't been for a while, but we've done some nasty prototype slinging to make it behave like it was. Now typescript types should be 1 to 1 with the actual classes yup exposes. In general this should not affect anything unless you are extending (via `addMethod` or otherwise) `mixed` prototype. ```diff import { - mixed, + Schema, } from 'yup'; - addMethod(mixed, 'method', impl) + addMethod(Schema, 'method', impl) ``` - concat works shallowly now. Previously concat functioned like a deep merge for object, which produced confusing behavior with incompatible concat'ed schema. Now concat for objects works similar to how it works for other types, the provided schema is applied on top of the existing schema, producing a new schema that is the same as calling each builder method in order - The builder object version of `when()` requires `then` and `otherwise` to be functions `(schema: Schema) => Schema`. - The function version of `when()` has been changed to make it easier to type. values are always passed as an array and schema, and options always the second and third argument. `this` is no longer set to the schema instance. and all functions _must_ return a schema to be type safe ```diff string() - .when('other', function (other) => { - if (other) return this.required() + .when('other', ([other], schema) => { + return other ? schema.required() : schema }) ``` # [1.0.0-alpha.3](https://github.com/jquense/yup/compare/v0.32.11...v1.0.0-alpha.3) (2021-12-28) ### Bug Fixes - schemaOf handles Dates ([c1fc816](https://github.com/jquense/yup/commit/c1fc816cdb03f7c9ff2e6745ff38a2b4f119d556)) - **types:** use type import/export ([#1238](https://github.com/jquense/yup/issues/1238)) ([bc284b5](https://github.com/jquense/yup/commit/bc284b5dbd4541464eb4a4edee73cb4d50c00fa7)) ### Features - More intuitive Object generics, faster types ([#1540](https://github.com/jquense/yup/issues/1540)) ([912e0be](https://github.com/jquense/yup/commit/912e0bed1e0184ba9c94015dc187eb6f86bb84d5)) # [1.0.0-alpha.2](https://github.com/jquense/yup/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2020-12-18) # [1.0.0-alpha.1](https://github.com/jquense/yup/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2020-12-18) ### Bug Fixes - **types:** make properties optional ([ba107cb](https://github.com/jquense/yup/commit/ba107cb50302e5245b960ed9a33f1c2167cc5d73)) - **types:** make properties optional ([495ae84](https://github.com/jquense/yup/commit/495ae84f8bfc22b9f4310700d4d8e9586584a4c7)) ### Features - remove unneeded Out type from schema ([0bf9732](https://github.com/jquense/yup/commit/0bf97327d406c9d982b2c0a93069bd047b53d5ef)) # [1.0.0-alpha.0](https://github.com/jquense/yup/compare/v0.32.8...v1.0.0-alpha.0) (2020-12-14) ### Features - add describe and meta to lazy, with resolve options ([e56fea3](https://github.com/jquense/yup/commit/e56fea3d09707e975fa1e3bc19fadaac4d8b065b)) ## [0.32.11](https://github.com/jquense/yup/compare/v0.32.10...v0.32.11) (2021-10-12) ### Bug Fixes * dep ranges ([2015c0f](https://github.com/jquense/yup/commit/2015c0f717065360076d5c460a139a2fff410166)) ## [0.32.10](https://github.com/jquense/yup/compare/v0.32.9...v0.32.10) (2021-10-11) ### Bug Fixes * carry over excluded edges when concating objects ([5334349](https://github.com/jquense/yup/commit/53343491f0624120812182a70919a2fc3ebe11f5)), closes [#1423](https://github.com/jquense/yup/issues/1423) * fix the typo for the array length validation ([#1287](https://github.com/jquense/yup/issues/1287)) ([4c17508](https://github.com/jquense/yup/commit/4c175086ce8e53df529bbdff6f215929a5a39167)) * missing transforms on concat ([f3056f2](https://github.com/jquense/yup/commit/f3056f2cbade92eaf0427848f43df97eae010555)), closes [#1260](https://github.com/jquense/yup/issues/1260) * oneOf, notOneOf swallowing multiple errors ([#1434](https://github.com/jquense/yup/issues/1434)) ([7842afb](https://github.com/jquense/yup/commit/7842afbaca0a44fc2fea72b44a90c2000ca2b8f0)) * prevent unhandled Promise rejection when returning rejected Promise inside test function ([#1327](https://github.com/jquense/yup/issues/1327)) ([5eda549](https://github.com/jquense/yup/commit/5eda549dfce95be225b0eb6dbe3cbe7bcd5d3347)) * SchemaOf<>'s treatment of Date objects. ([#1305](https://github.com/jquense/yup/issues/1305)) ([91ace1e](https://github.com/jquense/yup/commit/91ace1e8be3fc23c775ec8117c47b406bf29da4a)), closes [#1243](https://github.com/jquense/yup/issues/1243) [#1302](https://github.com/jquense/yup/issues/1302) * update lodash/lodash-es to fix CVEs flagged in 4.17.20 ([#1334](https://github.com/jquense/yup/issues/1334)) ([70d0b67](https://github.com/jquense/yup/commit/70d0b67e172f695168c5d00bc9856f2f775e0957)) * **utils:** use named functions for default exports ([#1329](https://github.com/jquense/yup/issues/1329)) ([acbb8b4](https://github.com/jquense/yup/commit/acbb8b4f3c24ceaf65eab09abaf8e086a9f11a73)) ### Features * add resolved to params ([#1437](https://github.com/jquense/yup/issues/1437)) ([03584f6](https://github.com/jquense/yup/commit/03584f6758ff43409113c41f58fd41e065aa18a3)) * add types to setLocale ([#1427](https://github.com/jquense/yup/issues/1427)) ([7576cd8](https://github.com/jquense/yup/commit/7576cd836ce9b660c054f9117795dbd9be12f747)), closes [#1321](https://github.com/jquense/yup/issues/1321) * allows custom types to be passed to avoid cast to ObjectSchema ([#1358](https://github.com/jquense/yup/issues/1358)) ([94cfd11](https://github.com/jquense/yup/commit/94cfd11b3f23e10f731efac05c5525829d10ded1)) ## [0.32.9](https://github.com/jquense/yup/compare/v0.32.6...v0.32.9) (2021-02-17) ### Bug Fixes * **types:** Array required() and defined() will no longer return any ([#1256](https://github.com/jquense/yup/issues/1256)) ([52e5876](https://github.com/jquense/yup/commit/52e5876)) * export MixedSchema to fix ts with --declarations ([#1204](https://github.com/jquense/yup/issues/1204)) ([67c96ae](https://github.com/jquense/yup/commit/67c96ae)) * **types:** add generic to Reference.create() ([#1208](https://github.com/jquense/yup/issues/1208)) ([be3d1b4](https://github.com/jquense/yup/commit/be3d1b4)) * **types:** reach and getIn make last 2 arguments optional ([#1194](https://github.com/jquense/yup/issues/1194)) ([5cf2c48](https://github.com/jquense/yup/commit/5cf2c48)) * do not initialize spec values with undefined ([#1177](https://github.com/jquense/yup/issues/1177)) ([e8e5b46](https://github.com/jquense/yup/commit/e8e5b46)), closes [jquense/yup#1160](https://github.com/jquense/yup/issues/1160) [jquense/yup#1160](https://github.com/jquense/yup/issues/1160) * **types:** meta() return type ([e41040a](https://github.com/jquense/yup/commit/e41040a)) * array handling in SchemaOf type ([#1169](https://github.com/jquense/yup/issues/1169)) ([e785e1a](https://github.com/jquense/yup/commit/e785e1a)) * **types:** make StringSchema.matches options optional ([#1166](https://github.com/jquense/yup/issues/1166)) ([b53e5f2](https://github.com/jquense/yup/commit/b53e5f2)) * **types:** SchemaOf doesn't produce a union of base schema ([2d71f32](https://github.com/jquense/yup/commit/2d71f32)) ## [0.32.6](https://github.com/jquense/yup/compare/v0.32.5...v0.32.6) (2020-12-08) ### Bug Fixes * mixed() is the the base class ([7f8591d](https://github.com/jquense/yup/commit/7f8591d)), closes [#1156](https://github.com/jquense/yup/issues/1156) ## [0.32.5](https://github.com/jquense/yup/compare/v0.32.4...v0.32.5) (2020-12-07) ### Bug Fixes * **types:** change base.default() to any ([01c6930](https://github.com/jquense/yup/commit/01c6930)) ## [0.32.4](https://github.com/jquense/yup/compare/v0.32.3...v0.32.4) (2020-12-07) ### Bug Fixes * **types:** rm base pick/omit types as they conflict with more specific ones ([14e2c8c](https://github.com/jquense/yup/commit/14e2c8c)) ### Features * add additional functions to Lazy class ([#1148](https://github.com/jquense/yup/issues/1148)) ([ecad1a3](https://github.com/jquense/yup/commit/ecad1a3)) ## [0.32.3](https://github.com/jquense/yup/compare/v0.32.2...v0.32.3) (2020-12-07) ### Bug Fixes * **types:** AnyObjectSchema anys ([1c54665](https://github.com/jquense/yup/commit/1c54665)) ## [0.32.2](https://github.com/jquense/yup/compare/v0.32.1...v0.32.2) (2020-12-07) ### Bug Fixes * **types:** array type with lazy ([ba92dfc](https://github.com/jquense/yup/commit/ba92dfc)), closes [#1146](https://github.com/jquense/yup/issues/1146) ## [0.32.1](https://github.com/jquense/yup/compare/v0.32.0...v0.32.1) (2020-12-04) ### Bug Fixes * cyclical import ([d5c5391](https://github.com/jquense/yup/commit/d5c5391)), closes [#1138](https://github.com/jquense/yup/issues/1138) * some strict fn type improvements ([8092218](https://github.com/jquense/yup/commit/8092218)) # [0.32.0](https://github.com/jquense/yup/compare/v0.31.1...v0.32.0) (2020-12-03) ### Features * typescript support ([#1134](https://github.com/jquense/yup/issues/1134)) ([b97c39d](https://github.com/jquense/yup/commit/b97c39d)) ### BREAKING CHANGES * `concat` doesn't check for "unset" nullable or presence when merging meaning the nullability and presence will always be the same as the schema passed to `concat()`. They can be overridden if needed after concatenation * schema factory functions are no longer constructors. The classes are now also exported for extension or whatever else. e.g. `import { StringSchema, string } from 'yup'` ## [0.31.1](https://github.com/jquense/yup/compare/v0.31.0...v0.31.1) (2020-12-01) ### Bug Fixes * swallowed errors on nested schema with no tests ([5316ab9](https://github.com/jquense/yup/commit/5316ab9)), closes [#1127](https://github.com/jquense/yup/issues/1127) ### Features * add `isTrue` and `isFalse` checks on boolean ([#910](https://github.com/jquense/yup/issues/910)) ([630a641](https://github.com/jquense/yup/commit/630a641)) # [0.31.0](https://github.com/jquense/yup/compare/v0.30.0...v0.31.0) (2020-11-23) ### Bug Fixes * path params incorrectly mutated ([ba23eb7](https://github.com/jquense/yup/commit/ba23eb7)), closes [#1122](https://github.com/jquense/yup/issues/1122) ### Features * add array.length() and treat empty arrays as valid for required() ([fbc158d](https://github.com/jquense/yup/commit/fbc158d)) * add object.pick and object.omit ([425705a](https://github.com/jquense/yup/commit/425705a)) * deprecate the getter overload of `default()` ([#1119](https://github.com/jquense/yup/issues/1119)) ([5dae837](https://github.com/jquense/yup/commit/5dae837)) * more strictly coerce strings, exclude arrays and plain objects ([963d2e8](https://github.com/jquense/yup/commit/963d2e8)) ### BREAKING CHANGES * array().required() will no longer consider an empty array missing and required checks will pass. To maintain the old behavior change to: ```js array().required().min(1) ``` * plain objects and arrays are no long cast to strings automatically to recreate the old behavior: ```js string().transform((_, input) => input != null && input.toString ? input.toString() : value); ``` # [0.30.0](https://github.com/jquense/yup/compare/v0.29.3...v0.30.0) (2020-11-19) ### Bug Fixes * defined() so it doesn't mark a schema as nullable ([f08d507](https://github.com/jquense/yup/commit/f08d507)) * IE11 clone() ([#1029](https://github.com/jquense/yup/issues/1029)) ([7fd80aa](https://github.com/jquense/yup/commit/7fd80aa)) * security Fix for Prototype Pollution - huntr.dev ([#1088](https://github.com/jquense/yup/issues/1088)) ([15a0f43](https://github.com/jquense/yup/commit/15a0f43)) * uuid's regexp ([#1112](https://github.com/jquense/yup/issues/1112)) ([57d42a8](https://github.com/jquense/yup/commit/57d42a8)) ### Features * exposes context on mixed.test function and add originalValue to context ([#1021](https://github.com/jquense/yup/issues/1021)) ([6096064](https://github.com/jquense/yup/commit/6096064)) ### Performance Improvements * reduce function calls for shallower stacks ([#1022](https://github.com/jquense/yup/issues/1022)) ([01da7e1](https://github.com/jquense/yup/commit/01da7e1)) ### BREAKING CHANGES * defined() now doesn't automatically allow null, this was a bug. to mimic the old behavior add nullable() to schema with defined() ## [0.29.3](https://github.com/jquense/yup/compare/v0.29.2...v0.29.3) (2020-08-04) ## [0.29.2](https://github.com/jquense/yup/compare/v0.29.1...v0.29.2) (2020-07-27) ### Bug Fixes * handle sparse array positions as undefined ([#950](https://github.com/jquense/yup/issues/950)) ([4e77348](https://github.com/jquense/yup/commit/4e77348)) ### Features * string UUID validation via a regex ([#909](https://github.com/jquense/yup/issues/909)) ([8f2bd2b](https://github.com/jquense/yup/commit/8f2bd2b)) ## [0.29.1](https://github.com/jquense/yup/compare/v0.29.0...v0.29.1) (2020-05-27) ### Bug Fixes * present checks for array and strings ([ecd8ebe](https://github.com/jquense/yup/commit/ecd8ebe483456805d743c888a82e180394ba8a22)), closes [#913](https://github.com/jquense/yup/issues/913) ### Features * allow access to parent schema (and unlimited ancestors!) in test context ([#556](https://github.com/jquense/yup/issues/556)) ([db35920](https://github.com/jquense/yup/commit/db35920b1ede4ea41ea90e1204b3da2a39787635)) # [0.29.0](https://github.com/jquense/yup/compare/v0.28.5...v0.29.0) (2020-05-19) * feat!: update docs to account for changes in types and add additional example (#891) ([e105a71](https://github.com/jquense/yup/commit/e105a71)), closes [#891](https://github.com/jquense/yup/issues/891) ### Bug Fixes * object bug when nested object has a property with strict ([#871](https://github.com/jquense/yup/issues/871)) ([7f52b8a](https://github.com/jquense/yup/commit/7f52b8a)) ### Features * expose oneOf and notOneOf values on description ([#885](https://github.com/jquense/yup/issues/885)) ([08dad5f](https://github.com/jquense/yup/commit/08dad5f)) ### BREAKING CHANGES * For users of `@types/yup` only, no function changes but the type def change is large enough that it warranted a major bump here ## [0.28.5](https://github.com/jquense/yup/compare/v0.28.4...v0.28.5) (2020-04-30) ### Bug Fixes - allow passing of function to .matches() options/message param ([#850](https://github.com/jquense/yup/issues/850)) ([16efe88](https://github.com/jquense/yup/commit/16efe88a8953db60438f77f43bd5bf614079803d)) - bug in object.noUnknown for nullish values https://github.com/jquense/yup/issues/854 ([#855](https://github.com/jquense/yup/issues/855)) ([ccb7c7d](https://github.com/jquense/yup/commit/ccb7c7d3c450537dffbb7d589e3111fc1f9a86fd)) ## [0.28.4](https://github.com/jquense/yup/compare/v0.28.3...v0.28.4) (2020-04-20) ### Bug Fixes - array reaching ([81e4058](https://github.com/jquense/yup/commit/81e4058)) ### Features - make schema.type and array.innerType public API's ([8f00d50](https://github.com/jquense/yup/commit/8f00d50)) - provide keys in default noUnknown message ([#579](https://github.com/jquense/yup/issues/579)) ([ad5d015](https://github.com/jquense/yup/commit/ad5d015)) ## [0.28.3](https://github.com/jquense/yup/compare/v0.28.2...v0.28.3) (2020-03-06) ### Bug Fixes - array.ensure ([94659c2](https://github.com/jquense/yup/commit/94659c2)), closes [#343](https://github.com/jquense/yup/issues/343) - match options ([493cc61](https://github.com/jquense/yup/commit/493cc61)), closes [#802](https://github.com/jquense/yup/issues/802) [#801](https://github.com/jquense/yup/issues/801) [#799](https://github.com/jquense/yup/issues/799) [#798](https://github.com/jquense/yup/issues/798) # [0.28.0](https://github.com/jquense/yup/compare/v0.26.10...v0.28.0) (2019-12-16) ### Bug Fixes - [#473](https://github.com/jquense/yup/issues/473) make concat compatible with (not)oneOf ([#492](https://github.com/jquense/yup/issues/492)) ([8d21cc9](https://github.com/jquense/yup/commit/8d21cc9)) - array path resolve for descendants ([#669](https://github.com/jquense/yup/issues/669)) ([d31e34d](https://github.com/jquense/yup/commit/d31e34d)) - change @babel/runtime version to be a range ([#488](https://github.com/jquense/yup/issues/488)) ([1c9b362](https://github.com/jquense/yup/commit/1c9b362)), closes [#486](https://github.com/jquense/yup/issues/486) - concat of mixed and subtype ([#444](https://github.com/jquense/yup/issues/444)) ([7705972](https://github.com/jquense/yup/commit/7705972)) - default message for test with object ([#453](https://github.com/jquense/yup/issues/453)) ([f1be37f](https://github.com/jquense/yup/commit/f1be37f)) - noUnknown() overriding ([#452](https://github.com/jquense/yup/issues/452)) ([3047b33](https://github.com/jquense/yup/commit/3047b33)) - string.matches() and regex global flag ([#450](https://github.com/jquense/yup/issues/450)) ([a8935b7](https://github.com/jquense/yup/commit/a8935b7)) - synchronous conditional object validation with unknown dependencies ([#598](https://github.com/jquense/yup/issues/598)) ([1081c41](https://github.com/jquense/yup/commit/1081c41)) - typo README (about excludeEmptyString) ([#441](https://github.com/jquense/yup/issues/441)) ([d02ff5e](https://github.com/jquense/yup/commit/d02ff5e)) - unix epoc bug in date parser ([#655](https://github.com/jquense/yup/issues/655)) ([0d14827](https://github.com/jquense/yup/commit/0d14827)) ### Features - add \_isFilled as overrideable `mixed` method to control required behavior ([#459](https://github.com/jquense/yup/issues/459)) ([5b01f18](https://github.com/jquense/yup/commit/5b01f18)) - add function test names to email and url ([#292](https://github.com/jquense/yup/issues/292)) ([7e94395](https://github.com/jquense/yup/commit/7e94395)) - aliases `optional()` and `unknown()` ([#460](https://github.com/jquense/yup/issues/460)) ([51e8661](https://github.com/jquense/yup/commit/51e8661)) - allow toggling strict() ([#457](https://github.com/jquense/yup/issues/457)) ([851d421](https://github.com/jquense/yup/commit/851d421)) - allow withMutation() nesting ([#456](https://github.com/jquense/yup/issues/456)) ([e53ea8c](https://github.com/jquense/yup/commit/e53ea8c)) - do concat in mutation mode ([#461](https://github.com/jquense/yup/issues/461)) ([02be4ca](https://github.com/jquense/yup/commit/02be4ca)) - finalize resolve() ([#447](https://github.com/jquense/yup/issues/447)) ([afc5119](https://github.com/jquense/yup/commit/afc5119)) - replace integer check with Number.isInteger ([#405](https://github.com/jquense/yup/issues/405)) ([1c18442](https://github.com/jquense/yup/commit/1c18442)) - support self references ([#443](https://github.com/jquense/yup/issues/443)) ([1cac515](https://github.com/jquense/yup/commit/1cac515)), closes [/github.com/jquense/yup/blob/d02ff5e59e004b4c5189d1b9fc0055cff45c61df/src/Reference.js#L3](https://github.com//github.com/jquense/yup/blob/d02ff5e59e004b4c5189d1b9fc0055cff45c61df/src/Reference.js/issues/L3) - use the alternate object index path syntax if the key contains dots (fixes [#536](https://github.com/jquense/yup/issues/536)) ([#539](https://github.com/jquense/yup/issues/539)) ([13e8c76](https://github.com/jquense/yup/commit/13e8c76)) ### BREAKING CHANGES - use Number.isInteger. This works correctly for large numbers. Related to https://github.com/jquense/yup/pull/147 - reach() no longer resolves the returned schema meaning it's conditions have not been processed yet; prefer validateAt/castAt where it makes sense - required no longer shows up twice in describe() output for array and strings, which also no longer override required # [0.27.0](https://github.com/jquense/yup/compare/v0.26.10...v0.27.0) (2019-03-14) ### Bug Fixes - change @babel/runtime version to be a range ([#488](https://github.com/jquense/yup/issues/488)) ([1c9b362](https://github.com/jquense/yup/commit/1c9b362)), closes [#486](https://github.com/jquense/yup/issues/486) - concat of mixed and subtype ([#444](https://github.com/jquense/yup/issues/444)) ([7705972](https://github.com/jquense/yup/commit/7705972)) - default message for test with object ([#453](https://github.com/jquense/yup/issues/453)) ([f1be37f](https://github.com/jquense/yup/commit/f1be37f)) - noUnknown() overriding ([#452](https://github.com/jquense/yup/issues/452)) ([3047b33](https://github.com/jquense/yup/commit/3047b33)) - typo README (about excludeEmptyString) ([#441](https://github.com/jquense/yup/issues/441)) ([d02ff5e](https://github.com/jquense/yup/commit/d02ff5e)) ### Features - add \_isFilled as overrideable `mixed` method to control required behavior ([#459](https://github.com/jquense/yup/issues/459)) ([5b01f18](https://github.com/jquense/yup/commit/5b01f18)) - aliases `optional()` and `unknown()` ([#460](https://github.com/jquense/yup/issues/460)) ([51e8661](https://github.com/jquense/yup/commit/51e8661)) - allow toggling strict() ([#457](https://github.com/jquense/yup/issues/457)) ([851d421](https://github.com/jquense/yup/commit/851d421)) - allow withMutation() nesting ([#456](https://github.com/jquense/yup/issues/456)) ([e53ea8c](https://github.com/jquense/yup/commit/e53ea8c)) - do concat in mutation mode ([#461](https://github.com/jquense/yup/issues/461)) ([02be4ca](https://github.com/jquense/yup/commit/02be4ca)) - finalize resolve() ([#447](https://github.com/jquense/yup/issues/447)) ([afc5119](https://github.com/jquense/yup/commit/afc5119)) - support self references ([#443](https://github.com/jquense/yup/issues/443)) ([1cac515](https://github.com/jquense/yup/commit/1cac515)), closes [/github.com/jquense/yup/blob/d02ff5e59e004b4c5189d1b9fc0055cff45c61df/src/Reference.js#L3](https://github.com//github.com/jquense/yup/blob/d02ff5e59e004b4c5189d1b9fc0055cff45c61df/src/Reference.js/issues/L3) ### BREAKING CHANGES - reach() no longer resolves the returned schema meaning it's conditions have not been processed yet; prefer validateAt/castAt where it makes sense - required no longer shows up twice in describe() output for array and strings, which also no longer override required ## v0.26.3 - Tue, 28 Aug 2018 15:00:04 GMT ## v0.26.0 - Fri, 20 Jul 2018 15:39:03 GMT ### BREAKING CHANGES - locale `number` config properties `less` and `more` are now `lessThan` and `moreThan` ## v0.25.1 - Wed, 16 May 2018 23:59:14 GMT ## v0.25.0 - Tue, 15 May 2018 21:43:54 GMT - remove default export, there are only named exports now! - fix message defaults for built-in tests, default is only used for `undefined` messages - fix the `describe()` method so it works with nested schemas ## v0.24.1 - Fri, 09 Feb 2018 19:09:02 GMT ## v0.24.0 - Tue, 16 Jan 2018 14:44:32 GMT - [f2a0b75](../../commit/f2a0b75), [061e590](../../commit/061e590) [added] number methods lessThan, moreThan ## v0.23.0 - Thu, 12 Oct 2017 17:08:47 GMT ** Probably not breaking but we are being safe about it ** - 🎉 Add Synchronous validation! [#94](https://github.com/jquense/yup/pull/94) ** Features ** - Custom locales without import order [#125](https://github.com/jquense/yup/pull/125) ## v0.22.1 - Thu, 12 Oct 2017 14:49:16 GMT - Fix bug in browsers without symbol [#132](https://github.com/jquense/yup/pull/132) ## v0.22.0 - Sat, 26 Aug 2017 14:48:57 GMT ** Breaking ** - Use native Set and lodash CloneDeep: [#109](https://github.com/jquense/yup/pull/109) \*\* Fixes and Features - Better custom locale support: [#105](https://github.com/jquense/yup/pull/105) - fix some messages: [#112](https://github.com/jquense/yup/pull/112) - Clearer errors for common mistakes: [#108](https://github.com/jquense/yup/pull/108) - New string validation length: [#67](https://github.com/jquense/yup/pull/67) ## v0.21.3 - Wed, 18 Jan 2017 15:39:25 GMT - [7bc01e0](../../commit/7bc01e0) [added] deep path support for `from` ## v0.21.2 - Fri, 09 Sep 2016 16:52:44 GMT - [be80413](../../commit/be80413) [fixed] default in concat() ## v0.21.1 - Mon, 29 Aug 2016 18:39:29 GMT ## v0.21.0 - Mon, 29 Aug 2016 18:29:31 GMT - [8a8cc5b](../../commit/8a8cc5b) [changed] remove case aliases and simplify camelCase ## v0.20.0 - Wed, 20 Jul 2016 02:02:08 GMT - [f7446d2](../../commit/f7446d2) [fixed] pass path correctly to cast() - [9b5232a](../../commit/9b5232a) [added] allow function then/otherwise bodies - [73858fe](../../commit/73858fe) [changed] Don't throw on undefined values in cast() ## v0.19.1 - Mon, 18 Jul 2016 21:53:05 GMT - [69c0ad4](../../commit/69c0ad4) [fixed] array().concat() incorrectly cleared the sub-schema ## v0.19.0 - Fri, 24 Jun 2016 15:19:48 GMT - [b0dd021](../../commit/b0dd021) [changed] Split integer(), remove transform - [758ac51](../../commit/758ac51) [added] string.ensure - [f2b0078](../../commit/f2b0078) [changed] Less aggressive type coercions - [ab94510](../../commit/ab94510) [fixed] boxed number allowed NaN ## v0.18.3 - Mon, 09 May 2016 15:50:47 GMT ## v0.18.2 - Mon, 25 Apr 2016 18:23:13 GMT ## v0.18.1 - Mon, 25 Apr 2016 15:01:16 GMT - [816e607](../../commit/816e607) [added] validation params to ValidationError ## v0.18.0 - Sat, 23 Apr 2016 01:20:27 GMT - [f827822](../../commit/f827822) [changed] validate() on objects won't cast nested schema with strict() ## v0.17.6 - Thu, 21 Apr 2016 14:59:59 GMT - [139dd24](../../commit/139dd24) [changed] lazy qualifies as a yup schema ## v0.17.5 - Thu, 21 Apr 2016 11:20:16 GMT - [c553cc0](../../commit/c553cc0) [added] options to lazy resolve ## v0.17.4 - Wed, 20 Apr 2016 14:15:39 GMT ## v0.17.3 - Tue, 19 Apr 2016 20:24:09 GMT - [6c309e4](../../commit/6c309e4) [fixed] array.ensure() ## v0.17.2 - Tue, 19 Apr 2016 16:46:54 GMT ## v0.17.1 - Thu, 14 Apr 2016 19:12:22 GMT - [ab78f54](../../commit/ab78f54) [fixed] reach with lazy() ## v0.17.0 - Thu, 14 Apr 2016 17:13:50 GMT - [6e9046b](../../commit/6e9046b) [changed] clean up interface, added lazy(), and fixed object strict semantics ## v0.16.5 - Tue, 12 Apr 2016 13:36:38 GMT - [c3b613b](../../commit/c3b613b) [added] strip() method for objects - [68fc010](../../commit/68fc010) [added] array.of shorthand ## v0.16.4 - Sat, 09 Apr 2016 20:13:13 GMT - [f30d1e3](../../commit/f30d1e3) [fixed] bug in date min/max with ref ## v0.16.3 - Thu, 07 Apr 2016 19:13:23 GMT ## v0.16.2 - Thu, 07 Apr 2016 17:57:44 GMT - [83c0656](../../commit/83c0656) [added] meta() and describe() ## v0.16.1 - Tue, 05 Apr 2016 20:56:45 GMT - [9d70a7b](../../commit/9d70a7b) [changed] doesn't throw when context is missing. - [594fa53](../../commit/594fa53) [changed] added reach error ## v0.16.0 - Tue, 05 Apr 2016 20:17:40 GMT - [75739b8](../../commit/75739b8) [added] context sensitive reach() ## v0.15.0 - Tue, 29 Mar 2016 14:56:15 GMT - [3ae5fdc](../../commit/3ae5fdc) [changed] `null` is not considered an empty value for isValid - [9eb42c6](../../commit/9eb42c6) [added] refs! ## v0.14.2 - Tue, 29 Mar 2016 14:48:37 GMT ## v0.14.1 - Tue, 16 Feb 2016 19:51:25 GMT - [ff19720](../../commit/ff19720) [fixed] noUnknown and stripUnknown work and propagate to children ## v0.14.0 - Mon, 08 Feb 2016 16:17:40 GMT - [86b6446](../../commit/86b6446) [fixed] camelCase should maintain leading underscores ## v0.13.0 - Mon, 01 Feb 2016 20:49:40 GMT - [335eb18](../../commit/335eb18) [fixed] pass options to array sub schema - [f7f631d](../../commit/f7f631d) [changed] oneOf doesn't include empty values - [0a7b2d4](../../commit/0a7b2d4) [fixed] type and whitelist/blacklist checks threw inconsistent errors - [1274a45](../../commit/1274a45) [changed] required() to non-exclusive ## v0.12.0 - Tue, 12 Jan 2016 19:12:18 GMT - [5bc250f](../../commit/5bc250f) [changed] don't clone unspecified object keys - [069c6fd](../../commit/069c6fd) [added] withMutation() method - [e1d4891](../../commit/e1d4891) [fixed] don't alias non existent fields ## v0.11.0 - Sun, 08 Nov 2015 17:17:09 GMT - [686f6b1](../../commit/686f6b1) [changed] concat() allows mixing "mixed" and other type ## 0.9.0 **breaking** - `test` functions are no longer passed `path` and `context` as arguments, Instead they are now values on `this` inside the test function. - test functions are longer called with the schema as their `this` value, use `this.schema` instead. **other changes** - test functions are call with with a new context object, including, options, parent and `createError` for dynamically altering validation errors. ## 0.8.3 - document `stripUnknown` - add `recursive` option - add `noUnknown()` test to objects ## 0.8.2 - default for objects now adds keys for all fields, not just fields with non empty defaults ## 0.8.1 - bug fix ## 0.8.0 **breaking** - `test` functions are now passed `path` and `context` values along with the field value. Only breaks if using the callback style of defining custom validations ## 0.7.0 **breaking** - the `validation()` method has been renamed to `test()` and has a new signature requiring a `name` argument - exclusive validations now trump the previous one instead of defering to it e.g: `string().max(10).max(15)` has a max of `15` instead of `10` **other changes** - expose advanced signature for custom validation tests, gives finer grained control over how tests are added - added the `abortEarly` (default: `true`) option - transforms are passed an addition parameter: 'originalValue' you allow recovering from a bad transform further up the chain (provided no one mutated the value) ## 0.6.3 - fix `concat()` method and add tests ## 0.6.2 - fix validations where nullable fields were failing due to `null` values e.g `string.max()` ## 0.6.1 - fix export error ## 0.6.0 **breaking** - Removed the `extend` and `create` methods. Use whatever javascript inheritance patterns you want instead. - the resolution order of defaults and coercions has changed. as well as the general handling of `null` values. - Number: `null` will coerce to `false` when `nullable()` is not specified. `NaN` values will now fail `isType()` checks - String: `null` will coerce to `''` when `nullable()` is not specified - Date: Invalid dates will not be coerced to `null`, but left as invalid date, This is probably not a problem for anyone as invalid dates will also fail `isType()` checks - default values are cloned everytime they are returned, so it is impossible to share references to defaults across schemas. No one should be doing that anyway - stopped pretending that using schemas as conditions in `when()` actually worked (it didn't) **other changes** - `transform()` now passes the original value to each transformer. Allowing you to recover from a bad transform. - added the `equals()` alias for `oneOf` - ## 0.5.0 **breaking** - isValid is now async, provide a node style callback, or use the promise the method returns to read the validity. This change allows for more robust validations, specifically remote ones for client code (or db queries for server code). The cast method is still, and will remain, synchronous. - **other changes** - added validate method (also async) which resolves to the value, and rejects with a new ValidationError ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2014 Jason Quense Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Yup Yup is a schema builder for runtime value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformation. > **You are viewing docs for the v1.0.0 of yup, pre-v1 docs are available: [here](https://github.com/jquense/yup/tree/pre-v1)** **Killer Features**: - Concise yet expressive schema interface, equipped to model simple to complex data models - Powerful TypeScript support. Infer static types from schema, or ensure schema correctly implement a type - Built-in async validation support. Model server-side and client-side validation equally well - Extensible: add your own type-safe methods and schema - Rich error details, make debugging a breeze - Compatible with [Standard Schema](https://github.com/standard-schema/standard-schema) ## Getting Started Schema are comprised of parsing actions (transforms) as well as assertions (tests) about the input value. Validate an input value to parse it and run the configured set of assertions. Chain together methods to build a schema. ```ts import { object, string, number, date, InferType } from 'yup'; let userSchema = object({ name: string().required(), age: number().required().positive().integer(), email: string().email(), website: string().url().nullable(), createdOn: date().default(() => new Date()), }); // parse and assert validity let user = await userSchema.validate(await fetchUser()); type User = InferType; /* { name: string; age: number; email?: string | undefined website?: string | null | undefined createdOn: Date }*/ ``` Use a schema to coerce or "cast" an input value into the correct type, and optionally transform that value into more concrete and specific values, without making further assertions. ```ts // Attempts to coerce values to the correct type let parsedUser = userSchema.cast({ name: 'jimmy', age: '24', createdOn: '2014-09-23T19:25:25Z', }); // ✅ { name: 'jimmy', age: 24, createdOn: Date } ``` Know that your input value is already parsed? You can "strictly" validate an input, and avoid the overhead of running parsing logic. ```ts // ❌ ValidationError "age is not a number" let parsedUser = await userSchema.validate( { name: 'jimmy', age: '24', }, { strict: true }, ); ``` ## Table of Contents - [Schema basics](#schema-basics) - [Parsing: Transforms](#parsing-transforms) - [Validation: Tests](#validation-tests) - [Customizing errors](#customizing-errors) - [Composition and Reuse](#composition-and-reuse) - [TypeScript integration](#typescript-integration) - [Schema defaults](#schema-defaults) - [Ensuring a schema matches an existing type](#ensuring-a-schema-matches-an-existing-type) - [Extending built-in schema with new methods](#extending-built-in-schema-with-new-methods) - [TypeScript configuration](#typescript-configuration) - [Error message customization](#error-message-customization) - [localization and i18n](#localization-and-i18n) - [Standard Schema Support](#standard-schema-support) - [API](#api) - [`yup`](#yup) - [`reach(schema: Schema, path: string, value?: object, context?: object): Schema`](#reachschema-schema-path-string-value-object-context-object-schema) - [`addMethod(schemaType: Schema, name: string, method: ()=> Schema): void`](#addmethodschematype-schema-name-string-method--schema-void) - [`ref(path: string, options: { contextPrefix: string }): Ref`](#refpath-string-options--contextprefix-string--ref) - [`lazy((value: any) => Schema): Lazy`](#lazyvalue-any--schema-lazy) - [`ValidationError(errors: string | Array, value: any, path: string)`](#validationerrorerrors-string--arraystring-value-any-path-string) - [`Schema`](#schema) - [`Schema.clone(): Schema`](#schemaclone-schema) - [`Schema.label(label: string): Schema`](#schemalabellabel-string-schema) - [`Schema.meta(metadata: SchemaMetadata): Schema`](#schemametametadata-schemametadata-schema) - [`Schema.describe(options?: ResolveOptions): SchemaDescription`](#schemadescribeoptions-resolveoptions-schemadescription) - [`Schema.concat(schema: Schema): Schema`](#schemaconcatschema-schema-schema) - [`Schema.validate(value: any, options?: object): Promise, ValidationError>`](#schemavalidatevalue-any-options-object-promiseinfertypeschema-validationerror) - [`Schema.validateSync(value: any, options?: object): InferType`](#schemavalidatesyncvalue-any-options-object-infertypeschema) - [`Schema.validateAt(path: string, value: any, options?: object): Promise, ValidationError>`](#schemavalidateatpath-string-value-any-options-object-promiseinfertypeschema-validationerror) - [`Schema.validateSyncAt(path: string, value: any, options?: object): InferType`](#schemavalidatesyncatpath-string-value-any-options-object-infertypeschema) - [`Schema.isValid(value: any, options?: object): Promise`](#schemaisvalidvalue-any-options-object-promiseboolean) - [`Schema.isValidSync(value: any, options?: object): boolean`](#schemaisvalidsyncvalue-any-options-object-boolean) - [`Schema.cast(value: any, options = {}): InferType`](#schemacastvalue-any-options---infertypeschema) - [`Schema.isType(value: any): value is InferType`](#schemaistypevalue-any-value-is-infertypeschema) - [`Schema.strict(enabled: boolean = false): Schema`](#schemastrictenabled-boolean--false-schema) - [`Schema.strip(enabled: boolean = true): Schema`](#schemastripenabled-boolean--true-schema) - [`Schema.withMutation(builder: (current: Schema) => void): void`](#schemawithmutationbuilder-current-schema--void-void) - [`Schema.default(value: any): Schema`](#schemadefaultvalue-any-schema) - [`Schema.getDefault(options?: object): Any`](#schemagetdefaultoptions-object-any) - [`Schema.nullable(message?: string | function): Schema`](#schemanullablemessage-string--function-schema) - [`Schema.nonNullable(message?: string | function): Schema`](#schemanonnullablemessage-string--function-schema) - [`Schema.defined(): Schema`](#schemadefined-schema) - [`Schema.optional(): Schema`](#schemaoptional-schema) - [`Schema.required(message?: string | function): Schema`](#schemarequiredmessage-string--function-schema) - [`Schema.notRequired(): Schema`](#schemanotrequired-schema) - [`Schema.typeError(message: string): Schema`](#schematypeerrormessage-string-schema) - [`Schema.oneOf(arrayOfValues: Array, message?: string | function): Schema` Alias: `equals`](#schemaoneofarrayofvalues-arrayany-message-string--function-schema-alias-equals) - [`Schema.notOneOf(arrayOfValues: Array, message?: string | function)`](#schemanotoneofarrayofvalues-arrayany-message-string--function) - [`Schema.when(keys: string | string[], builder: object | (values: any[], schema) => Schema): Schema`](#schemawhenkeys-string--string-builder-object--values-any-schema--schema-schema) - [`Schema.test(name: string, message: string | function | any, test: function): Schema`](#schematestname-string-message-string--function--any-test-function-schema) - [`Schema.test(options: object): Schema`](#schematestoptions-object-schema) - [`Schema.transform((currentValue: any, originalValue: any, schema: Schema, options: object) => any): Schema`](#schematransformcurrentvalue-any-originalvalue-any-schema-schema--options-object--any-schema) - [mixed](#mixed) - [string](#string) - [`string.required(message?: string | function): Schema`](#stringrequiredmessage-string--function-schema) - [`string.length(limit: number | Ref, message?: string | function): Schema`](#stringlengthlimit-number--ref-message-string--function-schema) - [`string.min(limit: number | Ref, message?: string | function): Schema`](#stringminlimit-number--ref-message-string--function-schema) - [`string.max(limit: number | Ref, message?: string | function): Schema`](#stringmaxlimit-number--ref-message-string--function-schema) - [`string.matches(regex: Regex, message?: string | function): Schema`](#stringmatchesregex-regex-message-string--function-schema) - [`string.matches(regex: Regex, options: { message: string, excludeEmptyString: bool }): Schema`](#stringmatchesregex-regex-options--message-string-excludeemptystring-bool--schema) - [`string.email(message?: string | function): Schema`](#stringemailmessage-string--function-schema) - [`string.url(message?: string | function): Schema`](#stringurlmessage-string--function-schema) - [`string.uuid(message?: string | function): Schema`](#stringuuidmessage-string--function-schema) - [`string.datetime(options?: {message?: string | function, allowOffset?: boolean, precision?: number})`](#stringdatetimeoptions-message-string--function-allowoffset-boolean-precision-number) - [`string.datetime(message?: string | function)`](#stringdatetimemessage-string--function) - [`string.ensure(): Schema`](#stringensure-schema) - [`string.trim(message?: string | function): Schema`](#stringtrimmessage-string--function-schema) - [`string.lowercase(message?: string | function): Schema`](#stringlowercasemessage-string--function-schema) - [`string.uppercase(message?: string | function): Schema`](#stringuppercasemessage-string--function-schema) - [number](#number) - [`number.min(limit: number | Ref, message?: string | function): Schema`](#numberminlimit-number--ref-message-string--function-schema) - [`number.max(limit: number | Ref, message?: string | function): Schema`](#numbermaxlimit-number--ref-message-string--function-schema) - [`number.lessThan(max: number | Ref, message?: string | function): Schema`](#numberlessthanmax-number--ref-message-string--function-schema) - [`number.moreThan(min: number | Ref, message?: string | function): Schema`](#numbermorethanmin-number--ref-message-string--function-schema) - [`number.positive(message?: string | function): Schema`](#numberpositivemessage-string--function-schema) - [`number.negative(message?: string | function): Schema`](#numbernegativemessage-string--function-schema) - [`number.integer(message?: string | function): Schema`](#numberintegermessage-string--function-schema) - [`number.truncate(): Schema`](#numbertruncate-schema) - [`number.round(type: 'floor' | 'ceil' | 'trunc' | 'round' = 'round'): Schema`](#numberroundtype-floor--ceil--trunc--round--round-schema) - [boolean](#boolean) - [date](#date) - [`date.min(limit: Date | string | Ref, message?: string | function): Schema`](#dateminlimit-date--string--ref-message-string--function-schema) - [`date.max(limit: Date | string | Ref, message?: string | function): Schema`](#datemaxlimit-date--string--ref-message-string--function-schema) - [array](#array) - [`array.of(type: Schema): this`](#arrayoftype-schema-this) - [`array.json(): this`](#arrayjson-this) - [`array.length(length: number | Ref, message?: string | function): this`](#arraylengthlength-number--ref-message-string--function-this) - [`array.min(limit: number | Ref, message?: string | function): this`](#arrayminlimit-number--ref-message-string--function-this) - [`array.max(limit: number | Ref, message?: string | function): this`](#arraymaxlimit-number--ref-message-string--function-this) - [`array.ensure(): this`](#arrayensure-this) - [`array.compact(rejector: (value) => boolean): Schema`](#arraycompactrejector-value--boolean-schema) - [tuple](#tuple) - [object](#object) - [Object schema defaults](#object-schema-defaults) - [`object.shape(fields: object, noSortEdges?: Array<[string, string]>): Schema`](#objectshapefields-object-nosortedges-arraystring-string-schema) - [`object.json(): this`](#objectjson-this) - [`object.concat(schemaB: ObjectSchema): ObjectSchema`](#objectconcatschemab-objectschema-objectschema) - [`object.pick(keys: string[]): Schema`](#objectpickkeys-string-schema) - [`object.omit(keys: string[]): Schema`](#objectomitkeys-string-schema) - [`object.from(fromKey: string, toKey: string, alias: boolean = false): this`](#objectfromfromkey-string-tokey-string-alias-boolean--false-this) - [`object.exact(message?: string | function): Schema`](#objectexactmessage-string--function-schema) - [`object.stripUnknown(): Schema`](#objectstripunknown-schema) - [`object.noUnknown(onlyKnownKeys: boolean = true, message?: string | function): Schema`](#objectnounknownonlyknownkeys-boolean--true-message-string--function-schema) - [`object.camelCase(): Schema`](#objectcamelcase-schema) - [`object.constantCase(): Schema`](#objectconstantcase-schema) ## Schema basics Schema definitions, are comprised of parsing "transforms" which manipulate inputs into the desired shape and type, "tests", which make assertions over parsed data. Schema also store a bunch of "metadata", details about the schema itself, which can be used to improve error messages, build tools that dynamically consume schema, or serialize schema into another format. In order to be maximally flexible yup allows running both parsing and assertions separately to match specific needs ### Parsing: Transforms Each built-in type implements basic type parsing, which comes in handy when parsing serialized data, such as JSON. Additionally types implement type specific transforms that can be enabled. ```ts let num = number().cast('1'); // 1 let obj = object({ firstName: string().lowercase().trim(), }) .json() .camelCase() .cast('{"first_name": "jAnE "}'); // { firstName: 'jane' } ``` Custom transforms can be added ```ts let reversedString = string() .transform((currentValue) => currentValue.split('').reverse().join('')) .cast('dlrow olleh'); // "hello world" ``` Transforms form a "pipeline", where the value of a previous transform is piped into the next one. When an input value is `undefined` yup will apply the schema default if it's configured. > Watch out! values are not guaranteed to be valid types in transform functions. Previous transforms > may have failed. For example a number transform may be receive the input value, `NaN`, or a number. ### Validation: Tests Yup schema run "tests" over input values. Tests assert that inputs conform to some criteria. Tests are distinct from transforms, in that they do not change or alter the input (or its type) and are usually reserved for checks that are hard, if not impossible, to represent in static types. ```ts string() .min(3, 'must be at least 3 characters long') .email('must be a valid email') .validate('no'); // ValidationError ``` As with transforms, tests can be customized on the fly ```ts let jamesSchema = string().test( 'is-james', (d) => `${d.path} is not James`, (value) => value == null || value === 'James', ); jamesSchema.validateSync('James'); // "James" jamesSchema.validateSync('Jane'); // ValidationError "this is not James" ``` > Heads up: unlike transforms, `value` in a custom test is guaranteed to be the correct type > (in this case an optional string). It still may be `undefined` or `null` depending on your schema > in those cases, you may want to return `true` for absent values unless your transform makes presence > related assertions. The test option `skipAbsent` will do this for you if set. #### Customizing errors In the simplest case a test function returns `true` or `false` depending on the whether the check passed. In the case of a failing test, yup will throw a [`ValidationError`](#validationerrorerrors-string--arraystring-value-any-path-string) with your (or the default) message for that test. ValidationErrors also contain a bunch of other metadata about the test, including it's name, what arguments (if any) it was called with, and the path to the failing field in the case of a nested validation. Error messages can also be constructed on the fly to customize how the schema fails. ```ts let order = object({ no: number().required(), sku: string().test({ name: 'is-sku', skipAbsent: true, test(value, ctx) { if (!value.startsWith('s-')) { return ctx.createError({ message: 'SKU missing correct prefix' }); } if (!value.endsWith('-42a')) { return ctx.createError({ message: 'SKU missing correct suffix' }); } if (value.length < 10) { return ctx.createError({ message: 'SKU is not the right length' }); } return true; }, }), }); order.validate({ no: 1234, sku: 's-1a45-14a' }); ``` ### Composition and Reuse Schema are immutable, each method call returns a new schema object. Reuse and pass them around without fear of mutating another instance. ```ts let optionalString = string().optional(); let definedString = optionalString.defined(); let value = undefined; optionalString.isValid(value); // true definedString.isValid(value); // false ``` ## TypeScript integration Yup schema produce static TypeScript interfaces. Use `InferType` to extract that interface: ```ts import * as yup from 'yup'; let personSchema = yup.object({ firstName: yup.string().defined(), nickName: yup.string().default('').nullable(), sex: yup .mixed() .oneOf(['male', 'female', 'other'] as const) .defined(), email: yup.string().nullable().email(), birthDate: yup.date().nullable().min(new Date(1900, 0, 1)), }); interface Person extends yup.InferType { // using interface instead of type generally gives nicer editor feedback } ``` ### Schema defaults A schema's default is used when casting produces an `undefined` output value. Because of this, setting a default affects the output type of the schema, essentially marking it as "defined()". ```ts import { string } from 'yup'; let value: string = string().default('hi').validate(undefined); // vs let value: string | undefined = string().validate(undefined); ``` ### Ensuring a schema matches an existing type In some cases a TypeScript type already exists, and you want to ensure that your schema produces a compatible type: ```ts import { object, number, string, ObjectSchema } from 'yup'; interface Person { name: string; age?: number; sex: 'male' | 'female' | 'other' | null; } // will raise a compile-time type error if the schema does not produce a valid Person let schema: ObjectSchema = object({ name: string().defined(), age: number().optional(), sex: string<'male' | 'female' | 'other'>().nullable().defined(), }); // ❌ errors: // "Type 'number | undefined' is not assignable to type 'string'." let badSchema: ObjectSchema = object({ name: number(), }); ``` ### Extending built-in schema with new methods You can use TypeScript's interface merging behavior to extend the schema types if needed. Type extensions should go in an "ambient" type definition file such as your `globals.d.ts`. Remember to actually extend the yup type in your application code! > Watch out! merging only works if the type definition is _exactly_ the same, including > generics. Consult the yup source code for each type to ensure you are defining it correctly ```ts // globals.d.ts declare module 'yup' { interface StringSchema { append(appendStr: string): this; } } // app.ts import { addMethod, string } from 'yup'; addMethod(string, 'append', function append(appendStr: string) { return this.transform((value) => `${value}${appendStr}`); }); string().append('~~~~').cast('hi'); // 'hi~~~~' ``` ### TypeScript configuration You **must** have the `strictNullChecks` compiler option enabled for type inference to work. We also recommend settings `strictFunctionTypes` to `false`, for functionally better types. Yes this reduces overall soundness, however TypeScript already disables this check for methods and constructors (note from TS docs): > During development of this feature, we discovered a large number of inherently > unsafe class hierarchies, including some in the DOM. Because of this, > the setting only applies to functions written in function syntax, not to those in method syntax: Your mileage will vary, but we've found that this check doesn't prevent many of real bugs, while increasing the amount of onerous explicit type casting in apps. ## Error message customization Default error messages can be customized for when no message is provided with a validation test. If any message is missing in the custom dictionary the error message will default to Yup's one. ```js import { setLocale } from 'yup'; setLocale({ mixed: { default: 'Não é válido', }, number: { min: 'Deve ser maior que ${min}', }, }); // now use Yup schemas AFTER you defined your custom dictionary let schema = yup.object().shape({ name: yup.string(), age: yup.number().min(18), }); try { await schema.validate({ name: 'jimmy', age: 11 }); } catch (err) { err.name; // => 'ValidationError' err.errors; // => ['Deve ser maior que 18'] } ``` ### localization and i18n If you need multi-language support, yup has got you covered. The function `setLocale` accepts functions that can be used to generate error objects with translation keys and values. These can be fed it into your favorite i18n library. ```js import { setLocale } from 'yup'; setLocale({ // use constant translation keys for messages without values mixed: { default: 'field_invalid', }, // use functions to generate an error object that includes the value from the schema number: { min: ({ min }) => ({ key: 'field_too_short', values: { min } }), max: ({ max }) => ({ key: 'field_too_big', values: { max } }), }, }); // ... let schema = yup.object().shape({ name: yup.string(), age: yup.number().min(18), }); try { await schema.validate({ name: 'jimmy', age: 11 }); } catch (err) { messages = err.errors.map((err) => i18next.t(err.key)); } ``` ## Standard Schema Support Yup is compatible with [Standard Schema](https://github.com/standard-schema/standard-schema). ## API ### `yup` The module export. ```ts // core schema import { mixed, string, number, boolean, bool, date, object, array, ref, lazy, } from 'yup'; // Classes import { Schema, MixedSchema, StringSchema, NumberSchema, BooleanSchema, DateSchema, ArraySchema, ObjectSchema, } from 'yup'; // Types import type { InferType, ISchema, AnySchema, AnyObjectSchema } from 'yup'; ``` #### `reach(schema: Schema, path: string, value?: object, context?: object): Schema` For nested schemas, `reach` will retrieve an inner schema based on the provided path. For nested schemas that need to resolve dynamically, you can provide a `value` and optionally a `context` object. ```js import { reach } from 'yup'; let schema = object({ nested: object({ arr: array(object({ num: number().max(4) })), }), }); reach(schema, 'nested.arr.num'); reach(schema, 'nested.arr[].num'); reach(schema, 'nested.arr[1].num'); reach(schema, 'nested["arr"][1].num'); ``` #### `addMethod(schemaType: Schema, name: string, method: ()=> Schema): void` Adds a new method to the core schema types. A friendlier convenience method for `schemaType.prototype[name] = method`. ```ts import { addMethod, date } from 'yup'; addMethod(date, 'format', function format(formats, parseStrict) { return this.transform((value, originalValue, ctx) => { if (ctx.isType(value)) return value; value = Moment(originalValue, formats, parseStrict); return value.isValid() ? value.toDate() : new Date(''); }); }); ``` If you want to add a method to ALL schema types, extend the abstract base class: `Schema` ```ts import { addMethod, Schema } from 'yup'; addMethod(Schema, 'myMethod', ...) ``` #### `ref(path: string, options: { contextPrefix: string }): Ref` Creates a reference to another sibling or sibling descendant field. Refs are resolved at _validation/cast time_ and supported where specified. Refs are evaluated in the proper order so that the ref value is resolved before the field using the ref (be careful of circular dependencies!). ```js import { ref, object, string } from 'yup'; let schema = object({ baz: ref('foo.bar'), foo: object({ bar: string(), }), x: ref('$x'), }); schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } }); // => { baz: 'boom', x: 5, foo: { bar: 'boom' } } ``` #### `lazy((value: any) => Schema): Lazy` Creates a schema that is evaluated at validation/cast time. Useful for creating recursive schema like Trees, for polymorphic fields and arrays. **CAUTION!** When defining parent-child recursive object schema, you want to reset the `default()` to `null` on the child—otherwise the object will infinitely nest itself when you cast it! ```js let node = object({ id: number(), child: yup.lazy(() => node.default(undefined)), }); let renderable = yup.lazy((value) => { switch (typeof value) { case 'number': return number(); case 'string': return string(); default: return mixed(); } }); let renderables = array().of(renderable); ``` #### `ValidationError(errors: string | Array, value: any, path: string)` Thrown on failed validations, with the following properties - `name`: "ValidationError" - `type`: the specific test type or test "name", that failed. - `value`: The field value that was tested; - `params`?: The test inputs, such as max value, regex, etc; - `path`: a string, indicating where there error was thrown. `path` is empty at the root level. - `errors`: array of error messages - `inner`: in the case of aggregate errors, inner is an array of `ValidationErrors` throw earlier in the validation chain. When the `abortEarly` option is `false` this is where you can inspect each error thrown, alternatively, `errors` will have all of the messages from each inner error. ### `Schema` `Schema` is the abstract base class that all schema type inherit from. It provides a number of base methods and properties to all other schema types. > Note: unless you are creating a custom schema type, Schema should never be used directly. For unknown/any types use [`mixed()`](#mixed) #### `Schema.clone(): Schema` Creates a deep copy of the schema. Clone is used internally to return a new schema with every schema state change. #### `Schema.label(label: string): Schema` Overrides the key name which is used in error messages. #### `Schema.meta(metadata: SchemaMetadata): Schema` Adds to a metadata object, useful for storing data with a schema, that doesn't belong to the cast object itself. A custom `SchemaMetadata` interface can be defined through [merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) with the `CustomSchemaMetadata` interface. Start by creating a `yup.d.ts` file in your package and creating your desired `CustomSchemaMetadata` interface: ```ts // yup.d.ts import 'yup'; declare module 'yup' { // Define your desired `SchemaMetadata` interface by merging the // `CustomSchemaMetadata` interface. export interface CustomSchemaMetadata { placeholderText?: string; tooltipText?: string; // … } } ``` #### `Schema.describe(options?: ResolveOptions): SchemaDescription` Collects schema details (like meta, labels, and active tests) into a serializable description object. ```ts let schema = object({ name: string().required(), }); let description = schema.describe(); ``` For schema with dynamic components (references, lazy, or conditions), describe requires more context to accurately return the schema description. In these cases provide `options` ```ts import { ref, object, string, boolean } from 'yup'; let schema = object({ isBig: boolean(), count: number().when('isBig', { is: true, then: (schema) => schema.min(5), otherwise: (schema) => schema.min(0), }), }); schema.describe({ value: { isBig: true } }); ``` And below are the description types, which differ a bit depending on the schema type. ```ts interface SchemaDescription { type: string; label?: string; meta: object | undefined; oneOf: unknown[]; notOneOf: unknown[]; default?: unknown; nullable: boolean; optional: boolean; tests: Array<{ name?: string; params: ExtraParams | undefined }>; // Present on object schema descriptions fields: Record; // Present on array schema descriptions innerType?: SchemaFieldDescription; } type SchemaFieldDescription = | SchemaDescription | SchemaRefDescription | SchemaLazyDescription; interface SchemaRefDescription { type: 'ref'; key: string; } interface SchemaLazyDescription { type: string; label?: string; meta: object | undefined; } ``` #### `Schema.concat(schema: Schema): Schema` Creates a new instance of the schema by combining two schemas. Only schemas of the same type can be concatenated. `concat` is not a "merge" function in the sense that all settings from the provided schema, override ones in the base, including type, presence and nullability. ```ts mixed().defined().concat(mixed().nullable()); // produces the equivalent to: mixed().defined().nullable(); ``` #### `Schema.validate(value: any, options?: object): Promise, ValidationError>` Returns the parses and validates an input value, returning the parsed value or throwing an error. This method is **asynchronous** and returns a Promise object, that is fulfilled with the value, or rejected with a `ValidationError`. ```js value = await schema.validate({ name: 'jimmy', age: 24 }); ``` Provide `options` to more specifically control the behavior of `validate`. ```js interface Options { // when true, parsing is skipped and the input is validated "as-is" strict: boolean = false; // Throw on the first error or collect and return all abortEarly: boolean = true; // Remove unspecified keys from objects stripUnknown: boolean = false; // when `false` validations will be performed shallowly recursive: boolean = true; // External values that can be provided to validations and conditionals context?: object; } ``` #### `Schema.validateSync(value: any, options?: object): InferType` Runs validatations synchronously _if possible_ and returns the resulting value, or throws a ValidationError. Accepts all the same options as `validate`. Synchronous validation only works if there are no configured async tests, e.g tests that return a Promise. For instance this will work: ```js let schema = number().test( 'is-42', "this isn't the number i want", (value) => value != 42, ); schema.validateSync(23); // throws ValidationError ``` however this will not: ```js let schema = number().test('is-42', "this isn't the number i want", (value) => Promise.resolve(value != 42), ); schema.validateSync(42); // throws Error ``` #### `Schema.validateAt(path: string, value: any, options?: object): Promise, ValidationError>` Validate a deeply nested path within the schema. Similar to how `reach` works, but uses the resulting schema as the subject for validation. > Note! The `value` here is the _root_ value relative to the starting schema, not the value at the nested path. ```js let schema = object({ foo: array().of( object({ loose: boolean(), bar: string().when('loose', { is: true, otherwise: (schema) => schema.strict(), }), }), ), }); let rootValue = { foo: [{ bar: 1 }, { bar: 1, loose: true }], }; await schema.validateAt('foo[0].bar', rootValue); // => ValidationError: must be a string await schema.validateAt('foo[1].bar', rootValue); // => '1' ``` #### `Schema.validateSyncAt(path: string, value: any, options?: object): InferType` Same as `validateAt` but synchronous. #### `Schema.isValid(value: any, options?: object): Promise` Returns `true` when the passed in value matches the schema. `isValid` is **asynchronous** and returns a Promise object. Takes the same options as `validate()`. #### `Schema.isValidSync(value: any, options?: object): boolean` Synchronously returns `true` when the passed in value matches the schema. Takes the same options as `validateSync()` and has the same caveats around async tests. #### `Schema.cast(value: any, options = {}): InferType` Attempts to coerce the passed in value to a value that matches the schema. For example: `'5'` will cast to `5` when using the `number()` type. Failed casts generally return `null`, but may also return results like `NaN` and unexpected strings. Provide `options` to more specifically control the behavior of `validate`. ```js interface CastOptions { // Remove undefined properties from objects stripUnknown: boolean = false; // Throws a TypeError if casting doesn't produce a valid type // note that the TS return type is inaccurate when this is `false`, use with caution assert?: boolean = true; // External values that used to resolve conditions and references context?: TContext; } ``` #### `Schema.isType(value: any): value is InferType` Runs a type check against the passed in `value`. It returns true if it matches, it does not cast the value. When `nullable()` is set `null` is considered a valid value of the type. You should use `isType` for all Schema type checks. #### `Schema.strict(enabled: boolean = false): Schema` Sets the `strict` option to `true`. Strict schemas skip coercion and transformation attempts, validating the value "as is". #### `Schema.strip(enabled: boolean = true): Schema` Marks a schema to be removed from an output object. Only works as a nested schema. ```js let schema = object({ useThis: number(), notThis: string().strip(), }); schema.cast({ notThis: 'foo', useThis: 4 }); // => { useThis: 4 } ``` Schema with `strip` enabled have an inferred type of `never`, allowing them to be removed from the overall type: ```ts let schema = object({ useThis: number(), notThis: string().strip(), }); InferType; /* { useThis?: number | undefined } */ ``` #### `Schema.withMutation(builder: (current: Schema) => void): void` First the legally required Rich Hickey quote: > If a tree falls in the woods, does it make a sound? > > If a pure function mutates some local data in order to produce an immutable return value, is that ok? `withMutation` allows you to mutate the schema in place, instead of the default behavior which clones before each change. Generally this isn't necessary since the vast majority of schema changes happen during the initial declaration, and only happen once over the lifetime of the schema, so performance isn't an issue. However certain mutations _do_ occur at cast/validation time, (such as conditional schema using [`when()`](#schemawhenkeys-string--string-builder-object--values-any-schema--schema-schema)), or when instantiating a schema object. ```js object() .shape({ key: string() }) .withMutation((schema) => { return arrayOfObjectTests.forEach((test) => { schema.test(test); }); }); ``` #### `Schema.default(value: any): Schema` Sets a default value to use when the value is `undefined`. Defaults are created after transformations are executed, but before validations, to help ensure that safe defaults are specified. The default value will be cloned on each use, which can incur performance penalty for objects and arrays. To avoid this overhead you can also pass a function that returns a new default. Note that `null` is considered a separate non-empty value. ```js yup.string.default('nothing'); yup.object.default({ number: 5 }); // object will be cloned every time a default is needed yup.object.default(() => ({ number: 5 })); // this is cheaper yup.date.default(() => new Date()); // also helpful for defaults that change over time ``` #### `Schema.getDefault(options?: object): Any` Retrieve a previously set default value. `getDefault` will resolve any conditions that may alter the default. Optionally pass `options` with `context` (for more info on `context` see `Schema.validate`). #### `Schema.nullable(message?: string | function): Schema` Indicates that `null` is a valid value for the schema. Without `nullable()` `null` is treated as a different type and will fail `Schema.isType()` checks. ```ts let schema = number().nullable(); schema.cast(null); // null InferType; // number | null ``` #### `Schema.nonNullable(message?: string | function): Schema` The opposite of `nullable`, removes `null` from valid type values for the schema. **Schema are non nullable by default**. ```ts let schema = number().nonNullable(); schema.cast(null); // TypeError InferType; // number ``` #### `Schema.defined(): Schema` Require a value for the schema. All field values apart from `undefined` meet this requirement. ```ts let schema = string().defined(); schema.cast(undefined); // TypeError InferType; // string ``` #### `Schema.optional(): Schema` The opposite of `defined()` allows `undefined` values for the given type. ```ts let schema = string().optional(); schema.cast(undefined); // undefined InferType; // string | undefined ``` #### `Schema.required(message?: string | function): Schema` Mark the schema as required, which will not allow `undefined` or `null` as a value. `required` negates the effects of calling `optional()` and `nullable()` > Watch out! [`string().required`](#stringrequiredmessage-string--function-schema)) works a little > different and additionally prevents empty string values (`''`) when required. #### `Schema.notRequired(): Schema` Mark the schema as not required. This is a shortcut for `schema.nullable().optional()`; #### `Schema.typeError(message: string): Schema` Define an error message for failed type checks. The `${value}` and `${type}` interpolation can be used in the `message` argument. #### `Schema.oneOf(arrayOfValues: Array, message?: string | function): Schema` Alias: `equals` Only allow values from set of values. Values added are removed from any `notOneOf` values if present. The `${values}` interpolation can be used in the `message` argument. If a ref or refs are provided, the `${resolved}` interpolation can be used in the message argument to get the resolved values that were checked at validation time. Note that `undefined` does not fail this validator, even when `undefined` is not included in `arrayOfValues`. If you don't want `undefined` to be a valid value, you can use `Schema.required`. ```js let schema = yup.mixed().oneOf(['jimmy', 42]); await schema.isValid(42); // => true await schema.isValid('jimmy'); // => true await schema.isValid(new Date()); // => false ``` #### `Schema.notOneOf(arrayOfValues: Array, message?: string | function)` Disallow values from a set of values. Values added are removed from `oneOf` values if present. The `${values}` interpolation can be used in the `message` argument. If a ref or refs are provided, the `${resolved}` interpolation can be used in the message argument to get the resolved values that were checked at validation time. ```js let schema = yup.mixed().notOneOf(['jimmy', 42]); await schema.isValid(42); // => false await schema.isValid(new Date()); // => true ``` #### `Schema.when(keys: string | string[], builder: object | (values: any[], schema) => Schema): Schema` Adjust the schema based on a sibling or sibling children fields. You can provide an object literal where the key `is` is value or a matcher function, `then` provides the true schema and/or `otherwise` for the failure condition. `is` conditions are strictly compared (`===`) if you want to use a different form of equality you can provide a function like: `is: (value) => value == true`. You can also prefix properties with `$` to specify a property that is dependent on `context` passed in by `validate()` or `cast` instead of the input value. `when` conditions are additive. `then` and `otherwise` are specified functions `(schema: Schema) => Schema`. ```js let schema = object({ isBig: boolean(), count: number() .when('isBig', { is: true, // alternatively: (val) => val == true then: (schema) => schema.min(5), otherwise: (schema) => schema.min(0), }) .when('$other', ([other], schema) => other === 4 ? schema.max(6) : schema, ), }); await schema.validate(value, { context: { other: 4 } }); ``` You can also specify more than one dependent key, in which case each value will be spread as an argument. ```js let schema = object({ isSpecial: boolean(), isBig: boolean(), count: number().when(['isBig', 'isSpecial'], { is: true, // alternatively: (isBig, isSpecial) => isBig && isSpecial then: (schema) => schema.min(5), otherwise: (schema) => schema.min(0), }), }); await schema.validate({ isBig: true, isSpecial: true, count: 10, }); ``` Alternatively you can provide a function that returns a schema, called with an array of values for each provided key the current schema. ```js let schema = yup.object({ isBig: yup.boolean(), count: yup.number().when('isBig', ([isBig], schema) => { return isBig ? schema.min(5) : schema.min(0); }), }); await schema.validate({ isBig: false, count: 4 }); ``` #### `Schema.test(name: string, message: string | function | any, test: function): Schema` Adds a test function to the validation chain. Tests are run after any object is cast. Many types have some tests built in, but you can create custom ones easily. In order to allow asynchronous custom validations _all_ (or no) tests are run asynchronously. A consequence of this is that test execution order cannot be guaranteed. All tests must provide a `name`, an error `message` and a validation function that must return `true` when the current `value` is valid and `false` or a `ValidationError` otherwise. To make a test async return a promise that resolves `true` or `false` or a `ValidationError`. For the `message` argument you can provide a string which will interpolate certain values if specified using the `${param}` syntax. By default all test messages are passed a `path` value which is valuable in nested schemas. The `test` function is called with the current `value`. For more advanced validations you can use the alternate signature to provide more options (see below): ```js let jimmySchema = string().test( 'is-jimmy', '${path} is not Jimmy', (value, context) => value === 'jimmy', ); // or make it async by returning a promise let asyncJimmySchema = string() .label('First name') .test( 'is-jimmy', ({ label }) => `${label} is not Jimmy`, // a message can also be a function async (value, testContext) => (await fetch('/is-jimmy/' + value)).responseText === 'true', ); await schema.isValid('jimmy'); // => true await schema.isValid('john'); // => false ``` Test functions are called with a special context value, as the second argument, that exposes some useful metadata and functions. For non arrow functions, the test context is also set as the function `this`. Watch out, if you access it via `this` it won't work in an arrow function. - `testContext.path`: the string path of the current validation - `testContext.schema`: the resolved schema object that the test is running against. - `testContext.options`: the `options` object that validate() or isValid() was called with - `testContext.parent`: in the case of nested schema, this is the value of the parent object - `testContext.originalValue`: the original value that is being tested - `testContext.createError(Object: { path: String, message: String, params: Object })`: create and return a validation error. Useful for dynamically setting the `path`, `params`, or more likely, the error `message`. If either option is omitted it will use the current path, or default message. #### `Schema.test(options: object): Schema` Alternative `test(..)` signature. `options` is an object containing some of the following options: ```js Options = { // unique name identifying the test name: string; // test function, determines schema validity test: (value: any) => boolean; // the validation error message message: string; // values passed to message for interpolation params: ?object; // mark the test as exclusive, meaning only one test of the same name can be active at once exclusive: boolean = false; } ``` In the case of mixing exclusive and non-exclusive tests the following logic is used. If a non-exclusive test is added to a schema with an exclusive test of the same name the exclusive test is removed and further tests of the same name will be stacked. If an exclusive test is added to a schema with non-exclusive tests of the same name the previous tests are removed and further tests of the same name will replace each other. ```js let max = 64; let schema = yup.string().test({ name: 'max', exclusive: true, params: { max }, message: '${path} must be less than ${max} characters', test: (value) => value == null || value.length <= max, }); ``` #### `Schema.transform((currentValue: any, originalValue: any, schema: Schema, options: object) => any): Schema` Adds a transformation to the transform chain. Transformations are central to the casting process, default transforms for each type coerce values to the specific type (as verified by [`isType()`](#schemaistypevalue-any-value-is-infertypeschema)). transforms are run before validations and only applied when the schema is not marked as `strict` (the default). Some types have built in transformations. Transformations are useful for arbitrarily altering how the object is cast, **however, you should take care not to mutate the passed in value.** Transforms are run sequentially so each `value` represents the current state of the cast, you can use the `originalValue` param if you need to work on the raw initial value. ```js let schema = string().transform((value, originalValue) => { return this.isType(value) && value !== null ? value.toUpperCase() : value; }); schema.cast('jimmy'); // => 'JIMMY' ``` Each types will handle basic coercion of values to the proper type for you, but occasionally you may want to adjust or refine the default behavior. For example, if you wanted to use a different date parsing strategy than the default one you could do that with a transform. ```js module.exports = function (formats = 'MMM dd, yyyy') { return date().transform((value, originalValue, context) => { // check to see if the previous transform already parsed the date if (context.isType(value)) return value; // the default coercion failed so let's try it with Moment.js instead value = Moment(originalValue, formats); // if it's valid return the date object, otherwise return an `InvalidDate` return value.isValid() ? value.toDate() : new Date(''); }); }; ``` ### mixed Creates a schema that matches all types, or just the ones you configure. Inherits from [`Schema`](#Schema). Mixed types extends `{}` by default instead of `any` or `unknown`. This is because in TypeScript `{}` means anything that isn't `null` or `undefined` which yup treats distinctly. ```ts import { mixed, InferType } from 'yup'; let schema = mixed().nullable(); schema.validateSync('string'); // 'string'; schema.validateSync(1); // 1; schema.validateSync(new Date()); // Date; InferType; // {} | undefined InferType; // {} | null ``` Custom types can be implemented by passing a type `check` function. This will also narrow the TypeScript type for the schema. ```ts import { mixed, InferType } from 'yup'; let objectIdSchema = yup .mixed((input): input is ObjectId => input instanceof ObjectId) .transform((value: any, input, ctx) => { if (ctx.isType(value)) return value; return new ObjectId(value); }); await objectIdSchema.validate(ObjectId('507f1f77bcf86cd799439011')); // ObjectId("507f1f77bcf86cd799439011") await objectIdSchema.validate('507f1f77bcf86cd799439011'); // ObjectId("507f1f77bcf86cd799439011") InferType; // ObjectId ``` ### string Define a string schema. Inherits from [`Schema`](#Schema). ```js let schema = yup.string(); await schema.isValid('hello'); // => true ``` By default, the `cast` logic of `string` is to call `toString` on the value if it exists. empty values are not coerced (use `ensure()` to coerce empty values to empty strings). Failed casts return the input value. #### `string.required(message?: string | function): Schema` The same as the `mixed()` schema required, **except** that empty strings are also considered 'missing' values. #### `string.length(limit: number | Ref, message?: string | function): Schema` Set a required length for the string value. The `${length}` interpolation can be used in the `message` argument #### `string.min(limit: number | Ref, message?: string | function): Schema` Set a minimum length limit for the string value. The `${min}` interpolation can be used in the `message` argument #### `string.max(limit: number | Ref, message?: string | function): Schema` Set a maximum length limit for the string value. The `${max}` interpolation can be used in the `message` argument #### `string.matches(regex: Regex, message?: string | function): Schema` Provide an arbitrary `regex` to match the value against. ```js let schema = string().matches(/(hi|bye)/); await schema.isValid('hi'); // => true await schema.isValid('nope'); // => false ``` #### `string.matches(regex: Regex, options: { message: string, excludeEmptyString: bool }): Schema` An alternate signature for `string.matches` with an options object. `excludeEmptyString`, when true, short circuits the regex test when the value is an empty string, making it a easier to avoid matching nothing without complicating the regex. ```js let schema = string().matches(/(hi|bye)/, { excludeEmptyString: true }); await schema.isValid(''); // => true ``` #### `string.email(message?: string | function): Schema` Validates the value as an email address using the same regex as defined by the HTML spec. WATCH OUT: Validating email addresses is nearly impossible with just code. Different clients and servers accept different things and many diverge from the various specs defining "valid" emails. The ONLY real way to validate an email address is to send a verification email to it and check that the user got it. With that in mind, yup picks a relatively simple regex that does not cover all cases, but aligns with browser input validation behavior since HTML forms are a common use case for yup. If you have more specific needs please override the email method with your own logic or regex: ```ts yup.addMethod(yup.string, 'email', function validateEmail(message) { return this.matches(myEmailRegex, { message, name: 'email', excludeEmptyString: true, }); }); ``` #### `string.url(message?: string | function): Schema` Validates the value as a valid URL via a regex. #### `string.uuid(message?: string | function): Schema` Validates the value as a valid UUID via a regex. #### `string.datetime(options?: {message?: string | function, allowOffset?: boolean, precision?: number})` Validates the value as an ISO datetime via a regex. Defaults to UTC validation; timezone offsets are not permitted (see `options.allowOffset`). Unlike `.date()`, `datetime` will not convert the string to a `Date` object. `datetime` also provides greater customization over the required format of the datetime string than `date` does. `options.allowOffset`: Allow a time zone offset. False requires UTC 'Z' timezone. _(default: false)_ `options.precision`: Require a certain sub-second precision on the date. _(default: null -- any (or no) sub-second precision)_ #### `string.datetime(message?: string | function)` An alternate signature for `string.datetime` that can be used when you don't need to pass options other than `message`. #### `string.ensure(): Schema` Transforms `undefined` and `null` values to an empty string along with setting the `default` to an empty string. #### `string.trim(message?: string | function): Schema` Transforms string values by removing leading and trailing whitespace. If `strict()` is set it will only validate that the value is trimmed. #### `string.lowercase(message?: string | function): Schema` Transforms the string value to lowercase. If `strict()` is set it will only validate that the value is lowercase. #### `string.uppercase(message?: string | function): Schema` Transforms the string value to uppercase. If `strict()` is set it will only validate that the value is uppercase. ### number Define a number schema. Inherits from [`Schema`](#Schema). ```js let schema = yup.number(); await schema.isValid(10); // => true ``` The default `cast` logic of `number` is: [`parseFloat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). Failed casts return `NaN`. #### `number.min(limit: number | Ref, message?: string | function): Schema` Set the minimum value allowed. The `${min}` interpolation can be used in the `message` argument. #### `number.max(limit: number | Ref, message?: string | function): Schema` Set the maximum value allowed. The `${max}` interpolation can be used in the `message` argument. #### `number.lessThan(max: number | Ref, message?: string | function): Schema` Value must be less than `max`. The `${less}` interpolation can be used in the `message` argument. #### `number.moreThan(min: number | Ref, message?: string | function): Schema` Value must be strictly greater than `min`. The `${more}` interpolation can be used in the `message` argument. #### `number.positive(message?: string | function): Schema` Value must be a positive number. #### `number.negative(message?: string | function): Schema` Value must be a negative number. #### `number.integer(message?: string | function): Schema` Validates that a number is an integer. #### `number.truncate(): Schema` Transformation that coerces the value to an integer by stripping off the digits to the right of the decimal point. #### `number.round(type: 'floor' | 'ceil' | 'trunc' | 'round' = 'round'): Schema` Adjusts the value via the specified method of `Math` (defaults to 'round'). ### boolean Define a boolean schema. Inherits from [`Schema`](#Schema). ```js let schema = yup.boolean(); await schema.isValid(true); // => true ``` ### date Define a Date schema. By default ISO date strings will parse correctly, for more robust parsing options see the extending schema types at the end of the readme. Inherits from [`Schema`](#Schema). ```js let schema = yup.date(); await schema.isValid(new Date()); // => true ``` The default `cast` logic of `date` is pass the value to the `Date` constructor, failing that, it will attempt to parse the date as an ISO date string. > If you would like ISO strings to not be cast to a `Date` object, use `.datetime()` instead. Failed casts return an invalid Date. #### `date.min(limit: Date | string | Ref, message?: string | function): Schema` Set the minimum date allowed. When a string is provided it will attempt to cast to a date first and use the result as the limit. #### `date.max(limit: Date | string | Ref, message?: string | function): Schema` Set the maximum date allowed, When a string is provided it will attempt to cast to a date first and use the result as the limit. ### array Define an array schema. Arrays can be typed or not, When specifying the element type, `cast` and `isValid` will apply to the elements as well. Options passed into `isValid` are also passed to child schemas. Inherits from [`Schema`](#Schema). ```js let schema = yup.array().of(yup.number().min(2)); await schema.isValid([2, 3]); // => true await schema.isValid([1, -24]); // => false schema.cast(['2', '3']); // => [2, 3] ``` You can also pass a subtype schema to the array constructor as a convenience. ```js array().of(yup.number()); // or array(yup.number()); ``` Arrays have no default casting behavior. #### `array.of(type: Schema): this` Specify the schema of array elements. `of()` is optional and when omitted the array schema will not validate its contents. #### `array.json(): this` Attempt to parse input string values as JSON using [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). #### `array.length(length: number | Ref, message?: string | function): this` Set a specific length requirement for the array. The `${length}` interpolation can be used in the `message` argument. #### `array.min(limit: number | Ref, message?: string | function): this` Set a minimum length limit for the array. The `${min}` interpolation can be used in the `message` argument. #### `array.max(limit: number | Ref, message?: string | function): this` Set a maximum length limit for the array. The `${max}` interpolation can be used in the `message` argument. #### `array.ensure(): this` Ensures that the value is an array, by setting the default to `[]` and transforming `null` and `undefined` values to an empty array as well. Any non-empty, non-array value will be wrapped in an array. ```js array().ensure().cast(null); // => [] array().ensure().cast(1); // => [1] array().ensure().cast([1]); // => [1] ``` #### `array.compact(rejector: (value) => boolean): Schema` Removes falsey values from the array. Providing a rejecter function lets you specify the rejection criteria yourself. ```js array().compact().cast(['', 1, 0, 4, false, null]); // => [1, 4] array() .compact(function (v) { return v == null; }) .cast(['', 1, 0, 4, false, null]); // => ['', 1, 0, 4, false] ``` ### tuple Tuples, are fixed length arrays where each item has a distinct type. Inherits from [`Schema`](#Schema). ```js import { tuple, string, number, InferType } from 'yup'; let schema = tuple([ string().label('name'), number().label('age').positive().integer(), ]); await schema.validate(['James', 3]); // ['James', 3] await schema.validate(['James', -24]); // => ValidationError: age must be a positive number InferType // [string, number] | undefined ``` tuples have no default casting behavior. ### object Define an object schema. Options passed into `isValid` are also passed to child schemas. Inherits from [`Schema`](#Schema). ```js yup.object({ name: string().required(), age: number().required().positive().integer(), email: string().email(), website: string().url(), }); ``` object schema do not have any default transforms applied. #### Object schema defaults Object schema come with a default value already set, which "builds" out the object shape, a sets any defaults for fields: ```js let schema = object({ name: string().default(''), }); schema.default(); // -> { name: '' } ``` This may be a bit surprising, but is usually helpful since it allows large, nested schema to create default values that fill out the whole shape and not just the root object. There is one gotcha! though. For nested object schema that are optional but include non optional fields may fail in unexpected ways: ```js let schema = object({ id: string().required(), names: object({ first: string().required(), }), }); schema.isValid({ id: 1 }); // false! names.first is required ``` This is because yup casts the input object before running validation which will produce: > `{ id: '1', names: { first: undefined }}` During the validation phase `names` exists, and is validated, finding `names.first` missing. If you wish to avoid this behavior do one of the following: - Set the nested default to undefined: `names.default(undefined)` - mark it nullable and default to null: `names.nullable().default(null)` #### `object.shape(fields: object, noSortEdges?: Array<[string, string]>): Schema` Define the keys of the object and the schemas for said keys. Note that you can chain `shape` method, which acts like `Object.assign`. ```ts object({ a: string(), b: number(), }).shape({ b: string(), c: number(), }); ``` would be exactly the same as: ```ts object({ a: string(), b: string(), c: number(), }); ``` #### `object.json(): this` Attempt to parse input string values as JSON using [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). #### `object.concat(schemaB: ObjectSchema): ObjectSchema` Creates a object schema, by applying all settings and fields from `schemaB` to the base, producing a new schema. The object shape is shallowly merged with common fields from `schemaB` taking precedence over the base fields. #### `object.pick(keys: string[]): Schema` Create a new schema from a subset of the original's fields. ```js let person = object({ age: number().default(30).required(), name: string().default('pat').required(), color: string().default('red').required(), }); let nameAndAge = person.pick(['name', 'age']); nameAndAge.getDefault(); // => { age: 30, name: 'pat'} ``` #### `object.omit(keys: string[]): Schema` Create a new schema with fields omitted. ```js let person = object({ age: number().default(30).required(), name: string().default('pat').required(), color: string().default('red').required(), }); let nameAndAge = person.omit(['color']); nameAndAge.getDefault(); // => { age: 30, name: 'pat'} ``` #### `object.from(fromKey: string, toKey: string, alias: boolean = false): this` Transforms the specified key to a new key. If `alias` is `true` then the old key will be left. ```js let schema = object({ myProp: mixed(), Other: mixed(), }) .from('prop', 'myProp') .from('other', 'Other', true); schema.cast({ prop: 5, other: 6 }); // => { myProp: 5, other: 6, Other: 6 } ``` #### `object.exact(message?: string | function): Schema` Validates that the object does not contain extra or unknown properties #### `object.stripUnknown(): Schema` The same as `object().validate(value, { stripUnknown: true})`, but as a transform method. When set any unknown properties will be removed. #### `object.noUnknown(onlyKnownKeys: boolean = true, message?: string | function): Schema` Validate that the object value only contains keys specified in `shape`, pass `false` as the first argument to disable the check. Restricting keys to known, also enables `stripUnknown` option, when not in strict mode. > Watch Out!: this method performs a transform and a validation, which may produce unexpected results. > For more explicit behavior use `object().stripUnknown` and `object().exact()` #### `object.camelCase(): Schema` Transforms all object keys to camelCase #### `object.constantCase(): Schema` Transforms all object keys to CONSTANT_CASE. ================================================ FILE: docs/extending.md ================================================ # Extending Schema For simple cases where you want to reuse common schema configurations, creating and passing around instances works great and is automatically typed correctly ```js import * as yup from 'yup'; const requiredString = yup.string().required().default(''); const momentDate = (parseFormats = ['MMM dd, yyy']) => yup.date().transform((value, originalValue, schema) => { if (schema.isType(value)) return value; // the default coercion transform failed so let's try it with Moment instead value = Moment(originalValue, parseFormats); return value.isValid() ? value.toDate() : yup.date.INVALID_DATE; }); export { momentDate, requiredString }; ``` Schema are immutable so each can be configured further without changing the original. ## Extending Schema with new methods `yup` provides a `addMethod()` utility for extending built-in schema: ```js function parseDateFromFormats(formats, parseStrict) { return this.transform((value, originalValue, schema) => { if (schema.isType(value)) return value; value = Moment(originalValue, formats, parseStrict); return value.isValid() ? value.toDate() : yup.date.INVALID_DATE; }); } yup.addMethod(yup.date, 'format', parseDateFromFormats); ``` Note that `addMethod` isn't magic, it mutates the prototype of the passed in schema. > Note: if you are using TypeScript you also need to adjust the class or interface > see the [typescript](./typescript.md) docs for details. ## Creating new Schema types If you're using case calls for creating an entirely new type, inheriting from an existing schema class may be best: Generally you should not be inheriting from the abstract `Schema` unless you know what you are doing. The other types are fair game though. You should keep in mind some basic guidelines when extending schemas: - never mutate an existing schema, always `clone()` and then mutate the new one before returning it. Built-in methods like `test` and `transform` take care of this for you, so you can safely use them (see below) without worrying - transforms should never mutate the `value` passed in, and should return an invalid object when one exists (`NaN`, `InvalidDate`, etc) instead of `null` for bad values. - by the time validations run, the `value` is guaranteed to be the correct type, however it still may be `null` or `undefined` ```js import { DateSchema } from 'yup'; class MomentDateSchema extends DateSchema { static create() { return MomentDateSchema(); } constructor() { super(); this._validFormats = []; this.withMutation(() => { this.transform(function (value, originalValue) { if (this.isType(value)) // we have a valid value return value; return Moment(originalValue, this._validFormats, true); }); }); } _typeCheck(value) { return ( super._typeCheck(value) || (moment.isMoment(value) && value.isValid()) ); } format(formats) { if (!formats) throw new Error('must enter a valid format'); let next = this.clone(); next._validFormats = {}.concat(formats); } } let schema = new MomentDateSchema(); schema.format('YYYY-MM-DD').cast('It is 2012-05-25'); // => Fri May 25 2012 00:00:00 GMT-0400 (Eastern Daylight Time) ``` ================================================ FILE: package.json ================================================ { "name": "yup", "version": "1.7.1", "description": "Dead simple Object schema validation", "main": "lib/index.js", "module": "lib/index.esm.js", "runkitExampleFilename": "./runkit-example.js", "scripts": { "test": "yarn lint && yarn testonly", "testonly": "vitest run", "test-sync": "vitest run --project sync", "tdd": "vitest --project async", "lint": "eslint src test", "precommit": "lint-staged", "toc": "doctoc README.md --github", "release": "rollout", "build:dts": "yarn tsc --emitDeclarationOnly -p . --outDir dts", "build": "rm -rf dts && yarn build:dts && yarn rollup -c rollup.config.js && yarn toc", "prepublishOnly": "yarn build" }, "repository": { "type": "git", "url": "git+https://github.com/jquense/yup.git" }, "author": { "name": "@monasticpanic Jason Quense" }, "license": "MIT", "bugs": { "url": "https://github.com/jquense/yup/issues" }, "homepage": "https://github.com/jquense/yup", "release": { "conventionalCommits": true, "publishDir": "lib" }, "prettier": { "singleQuote": true, "trailingComma": "all" }, "lint-staged": { "*.{js,json,css,md}": [ "prettier --write", "git add" ] }, "devDependencies": { "@4c/cli": "^4.0.4", "@4c/rollout": "patch:@4c/rollout@npm%3A4.0.2#~/.yarn/patches/@4c-rollout-npm-4.0.2-ab2b6d0bab.patch", "@4c/tsconfig": "^0.4.1", "@babel/cli": "^7.28.3", "@babel/core": "^7.28.4", "@babel/preset-typescript": "^7.27.1", "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-node-resolve": "^13.3.0", "@standard-schema/spec": "^1.0.0", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-jest": "^27.5.1", "babel-preset-env-modules": "^1.0.1", "doctoc": "^2.2.1", "dts-bundle-generator": "^6.13.0", "eslint": "^8.57.1", "eslint-config-jason": "^8.2.2", "eslint-config-prettier": "^8.10.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^25.7.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-ts-expect": "^2.1.0", "eslint-plugin-typescript": "^0.14.0", "hookem": "^2.0.1", "lint-staged": "^13.3.0", "prettier": "^2.8.8", "rollup": "^2.79.2", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-dts": "^4.2.3", "rollup-plugin-filesize": "^9.1.2", "rollup-plugin-node-resolve": "^5.2.0", "synchronous-promise": "^2.0.17", "typescript": "^4.9.5", "vitest": "^3.2.4" }, "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" }, "packageManager": "yarn@4.10.0+sha512.8dd111dbb1658cf17089636e5bf490795958158755f36cb75c5a2db0bda6be4d84b95447753627f3330d1457cb6f7e8c1e466eaed959073c82be0242c2cd41e7", "resolutions": { "@4c/rollout@npm:^4.0.2": "patch:@4c/rollout@npm%3A4.0.2#~/.yarn/patches/@4c-rollout-npm-4.0.2-ab2b6d0bab.patch" } } ================================================ FILE: renovate.json ================================================ { "extends": ["github>4Catalyzer/renovate-config:library", ":automergeMinor"] } ================================================ FILE: rollup.config.js ================================================ import nodeResolve from '@rollup/plugin-node-resolve'; import babel from '@rollup/plugin-babel'; import dts from 'rollup-plugin-dts'; import filesize from 'rollup-plugin-filesize'; const base = { input: './src/index.ts', plugins: [ nodeResolve({ extensions: ['.js', '.ts'] }), babel({ babelrc: true, envName: 'esm', extensions: ['.js', '.ts'], }), ], external: ['tiny-case', 'toposort', 'fn-name', 'property-expr'], }; module.exports = [ { input: './dts/index.d.ts', output: [{ file: 'lib/index.d.ts', format: 'es' }], plugins: [dts()], }, { ...base, output: [ { file: 'lib/index.js', format: 'cjs', }, { file: 'lib/index.esm.js', format: 'es', }, ], plugins: [...base.plugins, filesize()], }, ]; ================================================ FILE: src/Condition.ts ================================================ import isSchema from './util/isSchema'; import Reference from './Reference'; import type { ISchema } from './types'; export type ConditionBuilder> = ( values: any[], schema: T, options: ResolveOptions, ) => ISchema; export type ConditionConfig> = { is: any | ((...values: any[]) => boolean); then?: (schema: T) => ISchema; otherwise?: (schema: T) => ISchema; }; export type ResolveOptions = { value?: any; parent?: any; context?: TContext; }; class Condition = ISchema> { fn: ConditionBuilder; static fromOptions>( refs: Reference[], config: ConditionConfig, ) { if (!config.then && !config.otherwise) throw new TypeError( 'either `then:` or `otherwise:` is required for `when()` conditions', ); let { is, then, otherwise } = config; let check = typeof is === 'function' ? is : (...values: any[]) => values.every((value) => value === is); return new Condition(refs, (values, schema: any) => { let branch = check(...values) ? then : otherwise; return branch?.(schema) ?? schema; }); } constructor( public refs: readonly Reference[], builder: ConditionBuilder, ) { this.refs = refs; this.fn = builder; } resolve(base: TIn, options: ResolveOptions) { let values = this.refs.map((ref) => // TODO: ? operator here? ref.getValue(options?.value, options?.parent, options?.context), ); let schema = this.fn(values, base, options); if ( schema === undefined || // @ts-ignore this can be base schema === base ) { return base; } if (!isSchema(schema)) throw new TypeError('conditions must return a schema object'); return schema.resolve(options); } } export default Condition; ================================================ FILE: src/Lazy.ts ================================================ import isSchema from './util/isSchema'; import type { AnyObject, ISchema, ValidateOptions, NestedTestConfig, InferType, } from './types'; import type { ResolveOptions } from './Condition'; import type { CastOptionalityOptions, CastOptions, SchemaFieldDescription, SchemaLazyDescription, } from './schema'; import { Flags, Maybe, ResolveFlags } from './util/types'; import ValidationError from './ValidationError'; import Schema from './schema'; import { issuesFromValidationError, StandardResult, StandardSchemaProps, } from './standardSchema'; export type LazyBuilder< TSchema extends ISchema, TContext = AnyObject, > = (value: any, options: ResolveOptions) => TSchema; export function create< TSchema extends ISchema, TContext extends Maybe = AnyObject, >(builder: (value: any, options: ResolveOptions) => TSchema) { return new Lazy, TContext>(builder); } function catchValidationError(fn: () => any) { try { return fn(); } catch (err) { if (ValidationError.isError(err)) return Promise.reject(err); throw err; } } export interface LazySpec { meta: Record | undefined; optional: boolean; } class Lazy implements ISchema { type = 'lazy' as const; __isYupSchema__ = true; declare readonly __outputType: T; declare readonly __context: TContext; declare readonly __flags: TFlags; declare readonly __default: undefined; spec: LazySpec; constructor(private builder: any) { this.spec = { meta: undefined, optional: false }; } clone(spec?: Partial): Lazy { const next = new Lazy(this.builder); next.spec = { ...this.spec, ...spec }; return next; } private _resolve = ( value: any, options: ResolveOptions = {}, ): Schema => { let schema = this.builder(value, options) as Schema< T, TContext, undefined, TFlags >; if (!isSchema(schema)) throw new TypeError('lazy() functions must return a valid schema'); if (this.spec.optional) schema = schema.optional(); return schema.resolve(options); }; private optionality(optional: boolean) { const next = this.clone({ optional }); return next; } optional(): Lazy { return this.optionality(true); } resolve(options: ResolveOptions) { return this._resolve(options.value, options); } cast(value: any, options?: CastOptions): T; cast( value: any, options?: CastOptionalityOptions, ): T | null | undefined; cast( value: any, options?: CastOptions | CastOptionalityOptions, ): any { return this._resolve(value, options).cast(value, options as any); } asNestedTest(config: NestedTestConfig) { let { key, index, parent, options } = config; let value = parent[index ?? key!]; return this._resolve(value, { ...options, value, parent, }).asNestedTest(config); } validate(value: any, options?: ValidateOptions): Promise { return catchValidationError(() => this._resolve(value, options).validate(value, options), ); } validateSync(value: any, options?: ValidateOptions): T { return this._resolve(value, options).validateSync(value, options); } validateAt(path: string, value: any, options?: ValidateOptions) { return catchValidationError(() => this._resolve(value, options).validateAt(path, value, options), ); } validateSyncAt( path: string, value: any, options?: ValidateOptions, ) { return this._resolve(value, options).validateSyncAt(path, value, options); } isValid(value: any, options?: ValidateOptions) { try { return this._resolve(value, options).isValid(value, options); } catch (err) { if (ValidationError.isError(err)) { return Promise.resolve(false); } throw err; } } isValidSync(value: any, options?: ValidateOptions) { return this._resolve(value, options).isValidSync(value, options); } describe( options?: ResolveOptions, ): SchemaLazyDescription | SchemaFieldDescription { return options ? this.resolve(options).describe(options) : { type: 'lazy', meta: this.spec.meta, label: undefined }; } meta(): Record | undefined; meta(obj: Record): Lazy; meta(...args: [Record?]) { if (args.length === 0) return this.spec.meta; let next = this.clone(); next.spec.meta = Object.assign(next.spec.meta || {}, args[0]); return next; } get ['~standard']() { const schema = this; const standard: StandardSchemaProps< T, ResolveFlags > = { version: 1, vendor: 'yup', async validate( value: unknown, ): Promise>> { try { const result = await schema.validate(value, { abortEarly: false, }); return { value: result as ResolveFlags, }; } catch (err) { if (ValidationError.isError(err)) { return { issues: issuesFromValidationError(err), }; } throw err; } }, }; return standard; } } export default Lazy; ================================================ FILE: src/Reference.ts ================================================ import { getter } from 'property-expr'; import type { SchemaRefDescription } from './schema'; const prefixes = { context: '$', value: '.', } as const; export type ReferenceOptions = { map?: (value: unknown) => TValue; }; export function create( key: string, options?: ReferenceOptions, ) { return new Reference(key, options); } export default class Reference { readonly key: string; readonly isContext: boolean; readonly isValue: boolean; readonly isSibling: boolean; readonly path: any; readonly getter: (data: unknown) => unknown; readonly map?: (value: unknown) => TValue; declare readonly __isYupRef: boolean; constructor(key: string, options: ReferenceOptions = {}) { if (typeof key !== 'string') throw new TypeError('ref must be a string, got: ' + key); this.key = key.trim(); if (key === '') throw new TypeError('ref must be a non-empty string'); this.isContext = this.key[0] === prefixes.context; this.isValue = this.key[0] === prefixes.value; this.isSibling = !this.isContext && !this.isValue; let prefix = this.isContext ? prefixes.context : this.isValue ? prefixes.value : ''; this.path = this.key.slice(prefix.length); this.getter = this.path && getter(this.path, true); this.map = options.map; } getValue(value: any, parent?: {}, context?: {}): TValue { let result = this.isContext ? context : this.isValue ? value : parent; if (this.getter) result = this.getter(result || {}); if (this.map) result = this.map(result); return result; } /** * * @param {*} value * @param {Object} options * @param {Object=} options.context * @param {Object=} options.parent */ cast(value: any, options?: { parent?: {}; context?: {} }) { return this.getValue(value, options?.parent, options?.context); } resolve() { return this; } describe(): SchemaRefDescription { return { type: 'ref', key: this.key, }; } toString() { return `Ref(${this.key})`; } static isRef(value: any): value is Reference { return value && value.__isYupRef; } } // @ts-ignore Reference.prototype.__isYupRef = true; ================================================ FILE: src/ValidationError.ts ================================================ import printValue from './util/printValue'; import toArray from './util/toArray'; let strReg = /\$\{\s*(\w+)\s*\}/g; type Params = Record; class ValidationErrorNoStack implements ValidationError { name: string; message: string; value: any; path?: string; type?: string; params?: Params; errors: string[]; inner: ValidationError[]; constructor( errorOrErrors: string | ValidationError | readonly ValidationError[], value?: any, field?: string, type?: string, ) { this.name = 'ValidationError'; this.value = value; this.path = field; this.type = type; this.errors = []; this.inner = []; toArray(errorOrErrors).forEach((err) => { if (ValidationError.isError(err)) { this.errors.push(...err.errors); const innerErrors = err.inner.length ? err.inner : [err]; this.inner.push(...innerErrors); } else { this.errors.push(err); } }); this.message = this.errors.length > 1 ? `${this.errors.length} errors occurred` : this.errors[0]; } [Symbol.toStringTag] = 'Error'; } export default class ValidationError extends Error { value: any; path?: string; type?: string; params?: Params; errors: string[] = []; inner: ValidationError[] = []; static formatError( message: string | ((params: Params) => string) | unknown, params: Params, ) { // Attempt to make the path more friendly for error message interpolation. const path = params.label || params.path || 'this'; // Store the original path under `originalPath` so it isn't lost to custom // message functions; e.g., ones provided in `setLocale()` calls. params = { ...params, path, originalPath: params.path }; if (typeof message === 'string') return message.replace(strReg, (_, key) => printValue(params[key])); if (typeof message === 'function') return message(params); return message; } static isError(err: any): err is ValidationError { return err && err.name === 'ValidationError'; } constructor( errorOrErrors: string | ValidationError | readonly ValidationError[], value?: any, field?: string, type?: string, disableStack?: boolean, ) { const errorNoStack = new ValidationErrorNoStack( errorOrErrors, value, field, type, ); if (disableStack) { return errorNoStack; } super(); this.name = errorNoStack.name; this.message = errorNoStack.message; this.type = errorNoStack.type; this.value = errorNoStack.value; this.path = errorNoStack.path; this.errors = errorNoStack.errors; this.inner = errorNoStack.inner; if (Error.captureStackTrace) { Error.captureStackTrace(this, ValidationError); } } static [Symbol.hasInstance](inst: any) { return ( ValidationErrorNoStack[Symbol.hasInstance](inst) || super[Symbol.hasInstance](inst) ); } [Symbol.toStringTag] = 'Error'; } ================================================ FILE: src/array.ts ================================================ import isSchema from './util/isSchema'; import printValue from './util/printValue'; import parseJson from './util/parseJson'; import { array as locale } from './locale'; import type { AnyObject, InternalOptions, Message, ISchema, DefaultThunk, } from './types'; import type Reference from './Reference'; import type { Defined, Flags, NotNull, SetFlag, Maybe, Optionals, ToggleDefault, UnsetFlag, Concat, } from './util/types'; import Schema, { RunTest, SchemaInnerTypeDescription, SchemaSpec, } from './schema'; import type { ResolveOptions } from './Condition'; import type ValidationError from './ValidationError'; type InnerType = T extends Array ? I : never; export type RejectorFn = ( value: any, index: number, array: readonly any[], ) => boolean; export function create = AnyObject, T = any>( type?: ISchema, ) { return new ArraySchema(type as any); } interface ArraySchemaSpec extends SchemaSpec { types?: ISchema, TContext>; } export default class ArraySchema< TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = '', > extends Schema { declare spec: ArraySchemaSpec; readonly innerType?: ISchema, TContext>; constructor(type?: ISchema, TContext>) { super({ type: 'array', spec: { types: type } as ArraySchemaSpec, check(v: any): v is NonNullable { return Array.isArray(v); }, }); // `undefined` specifically means uninitialized, as opposed to "no subtype" this.innerType = type; } protected _cast(_value: any, _opts: InternalOptions) { const value = super._cast(_value, _opts); // should ignore nulls here if (!this._typeCheck(value) || !this.innerType) { return value; } let isChanged = false; const castArray = value.map((v, idx) => { const castElement = this.innerType!.cast(v, { ..._opts, path: `${_opts.path || ''}[${idx}]`, parent: value, originalValue: v, value: v, index: idx, }); if (castElement !== v) { isChanged = true; } return castElement; }); return isChanged ? castArray : value; } protected _validate( _value: any, options: InternalOptions = {}, panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ) { // let sync = options.sync; // let path = options.path; let innerType = this.innerType; // let endEarly = options.abortEarly ?? this.spec.abortEarly; let recursive = options.recursive ?? this.spec.recursive; let originalValue = options.originalValue != null ? options.originalValue : _value; super._validate(_value, options, panic, (arrayErrors, value) => { if (!recursive || !innerType || !this._typeCheck(value)) { next(arrayErrors, value); return; } originalValue = originalValue || value; // #950 Ensure that sparse array empty slots are validated let tests: RunTest[] = new Array(value.length); for (let index = 0; index < value.length; index++) { tests[index] = innerType!.asNestedTest({ options, index, parent: value, parentPath: options.path, originalParent: options.originalValue ?? _value, }); } this.runTests( { value, tests, originalValue: options.originalValue ?? _value, options, }, panic, (innerTypeErrors) => next(innerTypeErrors.concat(arrayErrors), value), ); }); } clone(spec?: SchemaSpec) { const next = super.clone(spec); // @ts-expect-error readonly next.innerType = this.innerType; return next; } /** Parse an input JSON string to an object */ json() { return this.transform(parseJson); } concat, IC, ID, IF extends Flags>( schema: ArraySchema, ): ArraySchema< Concat, TContext & IC, Extract extends never ? TDefault : ID, TFlags | IF >; concat(schema: this): this; concat(schema: any): any { let next = super.concat(schema) as this; // @ts-expect-error readonly next.innerType = this.innerType; if (schema.innerType) // @ts-expect-error readonly next.innerType = next.innerType ? // @ts-expect-error Lazy doesn't have concat and will break next.innerType.concat(schema.innerType) : schema.innerType; return next; } of( schema: ISchema, ): ArraySchema, TContext, TFlags> { // FIXME: this should return a new instance of array without the default to be let next = this.clone(); if (!isSchema(schema)) throw new TypeError( '`array.of()` sub-schema must be a valid yup schema not: ' + printValue(schema), ); // @ts-expect-error readonly next.innerType = schema; next.spec = { ...next.spec, types: schema as ISchema, TContext>, }; return next as any; } length( length: number | Reference, message: Message<{ length: number }> = locale.length, ) { return this.test({ message, name: 'length', exclusive: true, params: { length }, skipAbsent: true, test(value) { return value!.length === this.resolve(length); }, }); } min(min: number | Reference, message?: Message<{ min: number }>) { message = message || locale.min; return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, // FIXME(ts): Array test(value) { return value!.length >= this.resolve(min); }, }); } max(max: number | Reference, message?: Message<{ max: number }>) { message = message || locale.max; return this.test({ message, name: 'max', exclusive: true, params: { max }, skipAbsent: true, test(value) { return value!.length <= this.resolve(max); }, }); } ensure() { return this.default(() => [] as any).transform( (val: TIn, original: any) => { // We don't want to return `null` for nullable schema if (this._typeCheck(val)) return val; return original == null ? [] : [].concat(original); }, ); } compact(rejector?: RejectorFn) { let reject: RejectorFn = !rejector ? (v) => !!v : (v, i, a) => !rejector(v, i, a); return this.transform((values: readonly any[]) => values != null ? values.filter(reject) : values, ); } describe(options?: ResolveOptions) { const next = (options ? this.resolve(options) : this).clone(); const base = super.describe(options) as SchemaInnerTypeDescription; if (next.innerType) { let innerOptions = options; if (innerOptions?.value) { innerOptions = { ...innerOptions, parent: innerOptions.value, value: innerOptions.value[0], }; } base.innerType = next.innerType.describe(innerOptions); } return base; } } create.prototype = ArraySchema.prototype; export default interface ArraySchema< TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): ArraySchema>; defined(msg?: Message): ArraySchema, TContext, TDefault, TFlags>; optional(): ArraySchema; required( msg?: Message, ): ArraySchema, TContext, TDefault, TFlags>; notRequired(): ArraySchema, TContext, TDefault, TFlags>; nullable(msg?: Message): ArraySchema; nonNullable( msg?: Message, ): ArraySchema, TContext, TDefault, TFlags>; strip( enabled: false, ): ArraySchema>; strip( enabled?: true, ): ArraySchema>; } ================================================ FILE: src/boolean.ts ================================================ import Schema from './schema'; import type { AnyObject, DefaultThunk, Message } from './types'; import type { Defined, Flags, NotNull, SetFlag, ToggleDefault, UnsetFlag, Maybe, Optionals, } from './util/types'; import { boolean as locale } from './locale'; import isAbsent from './util/isAbsent'; export function create(): BooleanSchema; export function create< T extends boolean, TContext extends Maybe = AnyObject, >(): BooleanSchema; export function create() { return new BooleanSchema(); } export default class BooleanSchema< TType extends Maybe = boolean | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { constructor() { super({ type: 'boolean', check(v: any): v is NonNullable { if (v instanceof Boolean) v = v.valueOf(); return typeof v === 'boolean'; }, }); this.withMutation(() => { this.transform((value, _raw) => { if (this.spec.coerce && !this.isType(value)) { if (/^(true|1)$/i.test(String(value))) return true; if (/^(false|0)$/i.test(String(value))) return false; } return value; }); }); } isTrue( message = locale.isValue, ): BooleanSchema, TContext, TFlags> { return this.test({ message, name: 'is-value', exclusive: true, params: { value: 'true' }, test(value) { return isAbsent(value) || value === true; }, }) as any; } isFalse( message = locale.isValue, ): BooleanSchema, TContext, TFlags> { return this.test({ message, name: 'is-value', exclusive: true, params: { value: 'false' }, test(value) { return isAbsent(value) || value === false; }, }) as any; } override default>( def: DefaultThunk, ): BooleanSchema> { return super.default(def); } defined( msg?: Message, ): BooleanSchema, TContext, TDefault, TFlags> { return super.defined(msg); } optional(): BooleanSchema { return super.optional(); } required( msg?: Message, ): BooleanSchema, TContext, TDefault, TFlags> { return super.required(msg); } notRequired(): BooleanSchema, TContext, TDefault, TFlags> { return super.notRequired(); } nullable(): BooleanSchema { return super.nullable(); } nonNullable( msg?: Message, ): BooleanSchema, TContext, TDefault, TFlags> { return super.nonNullable(msg); } strip( enabled: false, ): BooleanSchema>; strip( enabled?: true, ): BooleanSchema>; strip(v: any) { return super.strip(v); } } create.prototype = BooleanSchema.prototype; ================================================ FILE: src/date.ts ================================================ import { parseIsoDate } from './util/parseIsoDate'; import { date as locale } from './locale'; import Ref from './Reference'; import type { AnyObject, DefaultThunk, Message } from './types'; import type { Defined, Flags, NotNull, SetFlag, Maybe, ToggleDefault, UnsetFlag, } from './util/types'; import Schema from './schema'; let invalidDate = new Date(''); let isDate = (obj: any): obj is Date => Object.prototype.toString.call(obj) === '[object Date]'; export function create(): DateSchema; export function create< T extends Date, TContext extends Maybe = AnyObject, >(): DateSchema; export function create() { return new DateSchema(); } export default class DateSchema< TType extends Maybe = Date | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { static INVALID_DATE = invalidDate; constructor() { super({ type: 'date', check(v: any): v is NonNullable { return isDate(v) && !isNaN(v.getTime()); }, }); this.withMutation(() => { this.transform((value, _raw) => { // null -> InvalidDate isn't useful; treat all nulls as null and let it fail on // nullability check vs TypeErrors if (!this.spec.coerce || this.isType(value) || value === null) return value; value = parseIsoDate(value); // 0 is a valid timestamp equivalent to 1970-01-01T00:00:00Z(unix epoch) or before. return !isNaN(value) ? new Date(value) : DateSchema.INVALID_DATE; }); }); } private prepareParam( ref: unknown | Ref, name: string, ): Date | Ref { let param: Date | Ref; if (!Ref.isRef(ref)) { let cast = this.cast(ref); if (!this._typeCheck(cast)) throw new TypeError( `\`${name}\` must be a Date or a value that can be \`cast()\` to a Date`, ); param = cast; } else { param = ref as Ref; } return param; } min(min: unknown | Ref, message = locale.min) { let limit = this.prepareParam(min, 'min'); return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, test(value) { return value! >= this.resolve(limit); }, }); } max(max: unknown | Ref, message = locale.max) { let limit = this.prepareParam(max, 'max'); return this.test({ message, name: 'max', exclusive: true, params: { max }, skipAbsent: true, test(value) { return value! <= this.resolve(limit); }, }); } } create.prototype = DateSchema.prototype; create.INVALID_DATE = invalidDate; export default interface DateSchema< TType extends Maybe, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): DateSchema>; concat>(schema: TOther): TOther; defined( msg?: Message, ): DateSchema, TContext, TDefault, TFlags>; optional(): DateSchema; required( msg?: Message, ): DateSchema, TContext, TDefault, TFlags>; notRequired(): DateSchema, TContext, TDefault, TFlags>; nullable(msg?: Message): DateSchema; nonNullable( msg?: Message, ): DateSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): DateSchema>; strip( enabled?: true, ): DateSchema>; } ================================================ FILE: src/globals.d.ts ================================================ ================================================ FILE: src/index.ts ================================================ import MixedSchema, { create as mixedCreate, MixedOptions, TypeGuard, } from './mixed'; import BooleanSchema, { create as boolCreate } from './boolean'; import StringSchema, { create as stringCreate } from './string'; import NumberSchema, { create as numberCreate } from './number'; import DateSchema, { create as dateCreate } from './date'; import ObjectSchema, { AnyObject, create as objectCreate } from './object'; import ArraySchema, { create as arrayCreate } from './array'; import TupleSchema, { create as tupleCreate } from './tuple'; import Reference, { create as refCreate } from './Reference'; import Lazy, { create as lazyCreate } from './Lazy'; import ValidationError from './ValidationError'; import reach, { getIn } from './util/reach'; import isSchema from './util/isSchema'; import printValue from './util/printValue'; import setLocale, { LocaleObject } from './setLocale'; import defaultLocale from './locale'; import Schema, { AnySchema, CastOptions as BaseCastOptions, SchemaSpec, SchemaRefDescription, SchemaInnerTypeDescription, SchemaObjectDescription, SchemaLazyDescription, SchemaFieldDescription, SchemaDescription, SchemaMetadata, CustomSchemaMetadata, } from './schema'; import type { AnyMessageParams, InferType, ISchema, Message, MessageParams, ValidateOptions, DefaultThunk, } from './types'; function addMethod>( schemaType: (...arg: any[]) => T, name: string, fn: (this: T, ...args: any[]) => T, ): void; function addMethod ISchema>( schemaType: T, name: string, fn: (this: InstanceType, ...args: any[]) => InstanceType, ): void; function addMethod(schemaType: any, name: string, fn: any) { if (!schemaType || !isSchema(schemaType.prototype)) throw new TypeError('You must provide a yup schema constructor function'); if (typeof name !== 'string') throw new TypeError('A Method name must be provided'); if (typeof fn !== 'function') throw new TypeError('Method function must be provided'); schemaType.prototype[name] = fn; } export type AnyObjectSchema = ObjectSchema; export type CastOptions = Omit; export type { AnyMessageParams, AnyObject, InferType, InferType as Asserts, ISchema, Message, MessageParams, AnySchema, MixedOptions, TypeGuard as MixedTypeGuard, SchemaSpec, SchemaRefDescription, SchemaInnerTypeDescription, SchemaObjectDescription, SchemaLazyDescription, SchemaFieldDescription, SchemaDescription, SchemaMetadata, CustomSchemaMetadata, LocaleObject, ValidateOptions, DefaultThunk, Lazy, Reference, }; export { mixedCreate as mixed, boolCreate as bool, boolCreate as boolean, stringCreate as string, numberCreate as number, dateCreate as date, objectCreate as object, arrayCreate as array, refCreate as ref, lazyCreate as lazy, tupleCreate as tuple, reach, getIn, isSchema, printValue, addMethod, setLocale, defaultLocale, ValidationError, }; export { Schema, MixedSchema, BooleanSchema, StringSchema, NumberSchema, DateSchema, ObjectSchema, ArraySchema, TupleSchema, Lazy as LazySchema, }; export type { CreateErrorOptions, TestContext, TestFunction, TestOptions, TestConfig, } from './util/createValidation'; export type { ObjectShape, TypeFromShape, DefaultFromShape, MakePartial, } from './util/objectTypes'; export type { Maybe, Flags, Optionals, ToggleDefault, Defined, NotNull, UnsetFlag, SetFlag, } from './util/types'; ================================================ FILE: src/locale.ts ================================================ import printValue from './util/printValue'; import { Message } from './types'; import ValidationError from './ValidationError'; export interface MixedLocale { default?: Message; required?: Message; oneOf?: Message<{ values: any }>; notOneOf?: Message<{ values: any }>; notNull?: Message; notType?: Message; defined?: Message; } export interface StringLocale { length?: Message<{ length: number }>; min?: Message<{ min: number }>; max?: Message<{ max: number }>; matches?: Message<{ regex: RegExp }>; email?: Message<{ regex: RegExp }>; url?: Message<{ regex: RegExp }>; uuid?: Message<{ regex: RegExp }>; datetime?: Message; datetime_offset?: Message; datetime_precision?: Message<{ precision: number }>; trim?: Message; lowercase?: Message; uppercase?: Message; } export interface NumberLocale { min?: Message<{ min: number }>; max?: Message<{ max: number }>; lessThan?: Message<{ less: number }>; moreThan?: Message<{ more: number }>; positive?: Message<{ more: number }>; negative?: Message<{ less: number }>; integer?: Message; } export interface DateLocale { min?: Message<{ min: Date | string }>; max?: Message<{ max: Date | string }>; } export interface ObjectLocale { noUnknown?: Message<{ unknown: string[] }>; exact?: Message<{ properties: string[] }>; } export interface ArrayLocale { length?: Message<{ length: number }>; min?: Message<{ min: number }>; max?: Message<{ max: number }>; } export interface TupleLocale { notType?: Message; } export interface BooleanLocale { isValue?: Message; } export interface LocaleObject { mixed?: MixedLocale; string?: StringLocale; number?: NumberLocale; date?: DateLocale; boolean?: BooleanLocale; object?: ObjectLocale; array?: ArrayLocale; tuple?: TupleLocale; } export let mixed: Required = { default: '${path} is invalid', required: '${path} is a required field', defined: '${path} must be defined', notNull: '${path} cannot be null', oneOf: '${path} must be one of the following values: ${values}', notOneOf: '${path} must not be one of the following values: ${values}', notType: ({ path, type, value, originalValue }) => { const castMsg = originalValue != null && originalValue !== value ? ` (cast from the value \`${printValue(originalValue, true)}\`).` : '.'; return type !== 'mixed' ? `${path} must be a \`${type}\` type, ` + `but the final value was: \`${printValue(value, true)}\`` + castMsg : `${path} must match the configured type. ` + `The validated value was: \`${printValue(value, true)}\`` + castMsg; }, }; export let string: Required = { length: '${path} must be exactly ${length} characters', min: '${path} must be at least ${min} characters', max: '${path} must be at most ${max} characters', matches: '${path} must match the following: "${regex}"', email: '${path} must be a valid email', url: '${path} must be a valid URL', uuid: '${path} must be a valid UUID', datetime: '${path} must be a valid ISO date-time', datetime_precision: '${path} must be a valid ISO date-time with a sub-second precision of exactly ${precision} digits', datetime_offset: '${path} must be a valid ISO date-time with UTC "Z" timezone', trim: '${path} must be a trimmed string', lowercase: '${path} must be a lowercase string', uppercase: '${path} must be a upper case string', }; export let number: Required = { min: '${path} must be greater than or equal to ${min}', max: '${path} must be less than or equal to ${max}', lessThan: '${path} must be less than ${less}', moreThan: '${path} must be greater than ${more}', positive: '${path} must be a positive number', negative: '${path} must be a negative number', integer: '${path} must be an integer', }; export let date: Required = { min: '${path} field must be later than ${min}', max: '${path} field must be at earlier than ${max}', }; export let boolean: BooleanLocale = { isValue: '${path} field must be ${value}', }; export let object: Required = { noUnknown: '${path} field has unspecified keys: ${unknown}', exact: '${path} object contains unknown properties: ${properties}', }; export let array: Required = { min: '${path} field must have at least ${min} items', max: '${path} field must have less than or equal to ${max} items', length: '${path} must have ${length} items', }; export let tuple: Required = { notType: (params) => { const { path, value, spec } = params; const typeLen = spec.types.length; if (Array.isArray(value)) { if (value.length < typeLen) return `${path} tuple value has too few items, expected a length of ${typeLen} but got ${ value.length } for value: \`${printValue(value, true)}\``; if (value.length > typeLen) return `${path} tuple value has too many items, expected a length of ${typeLen} but got ${ value.length } for value: \`${printValue(value, true)}\``; } return ValidationError.formatError(mixed.notType, params); }, }; export default Object.assign(Object.create(null), { mixed, string, number, date, object, array, boolean, tuple, }) as LocaleObject; ================================================ FILE: src/mixed.ts ================================================ import { AnyObject, DefaultThunk, Message } from './types'; import type { Concat, Defined, Flags, SetFlag, Maybe, ToggleDefault, UnsetFlag, } from './util/types'; import Schema from './schema'; const returnsTrue: any = () => true; type AnyPresentValue = {}; export type TypeGuard = (value: any) => value is NonNullable; export interface MixedOptions { type?: string; check?: TypeGuard; } export function create( spec?: MixedOptions | TypeGuard, ) { return new MixedSchema(spec); } export default class MixedSchema< TType extends Maybe = AnyPresentValue | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { constructor(spec?: MixedOptions | TypeGuard) { super( typeof spec === 'function' ? { type: 'mixed', check: spec } : { type: 'mixed', check: returnsTrue as TypeGuard, ...spec }, ); } } export default interface MixedSchema< TType extends Maybe = AnyPresentValue | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): MixedSchema>; concat( schema: MixedSchema, ): MixedSchema, TContext & IC, ID, TFlags | IF>; concat( schema: Schema, ): MixedSchema, TContext & IC, ID, TFlags | IF>; concat(schema: this): this; defined( msg?: Message, ): MixedSchema, TContext, TDefault, TFlags>; optional(): MixedSchema; required( msg?: Message, ): MixedSchema, TContext, TDefault, TFlags>; notRequired(): MixedSchema, TContext, TDefault, TFlags>; nullable( msg?: Message, ): MixedSchema; nonNullable( msg?: Message, ): MixedSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): MixedSchema>; strip( enabled?: true, ): MixedSchema>; } create.prototype = MixedSchema.prototype; ================================================ FILE: src/number.ts ================================================ import { number as locale } from './locale'; import isAbsent from './util/isAbsent'; import type { AnyObject, DefaultThunk, Message } from './types'; import type Reference from './Reference'; import type { Concat, Defined, Flags, NotNull, SetFlag, Maybe, ToggleDefault, UnsetFlag, } from './util/types'; import Schema from './schema'; let isNaN = (value: Maybe) => value != +value!; export function create(): NumberSchema; export function create< T extends number, TContext extends Maybe = AnyObject, >(): NumberSchema; export function create() { return new NumberSchema(); } export default class NumberSchema< TType extends Maybe = number | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { constructor() { super({ type: 'number', check(value: any): value is NonNullable { if (value instanceof Number) value = value.valueOf(); return typeof value === 'number' && !isNaN(value); }, }); this.withMutation(() => { this.transform((value, _raw) => { if (!this.spec.coerce) return value; let parsed = value; if (typeof parsed === 'string') { parsed = parsed.replace(/\s/g, ''); if (parsed === '') return NaN; // don't use parseFloat to avoid positives on alpha-numeric strings parsed = +parsed; } // null -> NaN isn't useful; treat all nulls as null and let it fail on // nullability check vs TypeErrors if (this.isType(parsed) || parsed === null) return parsed; return parseFloat(parsed); }); }); } min(min: number | Reference, message = locale.min) { return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, test(value: Maybe) { return value! >= this.resolve(min); }, }); } max(max: number | Reference, message = locale.max) { return this.test({ message, name: 'max', exclusive: true, params: { max }, skipAbsent: true, test(value: Maybe) { return value! <= this.resolve(max); }, }); } lessThan(less: number | Reference, message = locale.lessThan) { return this.test({ message, name: 'max', exclusive: true, params: { less }, skipAbsent: true, test(value: Maybe) { return value! < this.resolve(less); }, }); } moreThan(more: number | Reference, message = locale.moreThan) { return this.test({ message, name: 'min', exclusive: true, params: { more }, skipAbsent: true, test(value: Maybe) { return value! > this.resolve(more); }, }); } positive(msg = locale.positive) { return this.moreThan(0, msg); } negative(msg = locale.negative) { return this.lessThan(0, msg); } integer(message = locale.integer) { return this.test({ name: 'integer', message, skipAbsent: true, test: (val) => Number.isInteger(val), }); } truncate() { return this.transform((value) => (!isAbsent(value) ? value | 0 : value)); } round(method?: 'ceil' | 'floor' | 'round' | 'trunc') { let avail = ['ceil', 'floor', 'round', 'trunc']; method = (method?.toLowerCase() as any) || ('round' as const); // this exists for symemtry with the new Math.trunc if (method === 'trunc') return this.truncate(); if (avail.indexOf(method!.toLowerCase()) === -1) throw new TypeError( 'Only valid options for round() are: ' + avail.join(', '), ); return this.transform((value) => !isAbsent(value) ? Math[method!](value) : value, ); } } create.prototype = NumberSchema.prototype; // // Number Interfaces // export default interface NumberSchema< TType extends Maybe = number | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): NumberSchema>; concat, UContext, UFlags extends Flags, UDefault>( schema: NumberSchema, ): NumberSchema< Concat, TContext & UContext, UDefault, TFlags | UFlags >; concat(schema: this): this; defined( msg?: Message, ): NumberSchema, TContext, TDefault, TFlags>; optional(): NumberSchema; required( msg?: Message, ): NumberSchema, TContext, TDefault, TFlags>; notRequired(): NumberSchema, TContext, TDefault, TFlags>; nullable( msg?: Message, ): NumberSchema; nonNullable( msg?: Message, ): NumberSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): NumberSchema>; strip( enabled?: true, ): NumberSchema>; } ================================================ FILE: src/object.ts ================================================ // @ts-ignore import { getter, normalizePath, join } from 'property-expr'; import { camelCase, snakeCase } from 'tiny-case'; import { Flags, Maybe, SetFlag, ToggleDefault, UnsetFlag } from './util/types'; import { object as locale } from './locale'; import sortFields from './util/sortFields'; import sortByKeyOrder from './util/sortByKeyOrder'; import { DefaultThunk, InternalOptions, ISchema, Message } from './types'; import type { Defined, NotNull, _ } from './util/types'; import Reference from './Reference'; import Schema, { SchemaObjectDescription, SchemaSpec } from './schema'; import { ResolveOptions } from './Condition'; import type { AnyObject, ConcatObjectTypes, DefaultFromShape, MakePartial, MergeObjectTypes, ObjectShape, PartialDeep, TypeFromShape, } from './util/objectTypes'; import parseJson from './util/parseJson'; import type { Test } from './util/createValidation'; import type ValidationError from './ValidationError'; export type { AnyObject }; type MakeKeysOptional = T extends AnyObject ? _> : T; export type Shape, C = any> = { [field in keyof T]-?: ISchema | Reference; }; export type ObjectSchemaSpec = SchemaSpec & { noUnknown?: boolean; }; function deepPartial(schema: any) { if ('fields' in schema) { const partial: any = {}; for (const [key, fieldSchema] of Object.entries(schema.fields)) { partial[key] = deepPartial(fieldSchema); } return schema.setFields(partial); } if (schema.type === 'array') { const nextArray = schema.optional(); if (nextArray.innerType) nextArray.innerType = deepPartial(nextArray.innerType); return nextArray; } if (schema.type === 'tuple') { return schema .optional() .clone({ types: schema.spec.types.map(deepPartial) }); } if ('optional' in schema) { return schema.optional(); } return schema; } const deepHas = (obj: any, p: string) => { const path = [...normalizePath(p)]; if (path.length === 1) return path[0] in obj; let last = path.pop()!; let parent = getter(join(path), true)(obj); return !!(parent && last in parent); }; let isObject = (obj: any): obj is Record => Object.prototype.toString.call(obj) === '[object Object]'; function unknown(ctx: ObjectSchema, value: any) { let known = Object.keys(ctx.fields); return Object.keys(value).filter((key) => known.indexOf(key) === -1); } const defaultSort = sortByKeyOrder([]); export function create< C extends Maybe = AnyObject, S extends ObjectShape = {}, >(spec?: S) { type TIn = _>; type TDefault = _>; return new ObjectSchema(spec as any); } export default interface ObjectSchema< TIn extends Maybe, TContext = AnyObject, // important that this is `any` so that using `ObjectSchema`'s default // will match object schema regardless of defaults TDefault = any, TFlags extends Flags = '', > extends Schema, TContext, TDefault, TFlags> { default>( def: DefaultThunk, ): ObjectSchema>; defined( msg?: Message, ): ObjectSchema, TContext, TDefault, TFlags>; optional(): ObjectSchema; required( msg?: Message, ): ObjectSchema, TContext, TDefault, TFlags>; notRequired(): ObjectSchema, TContext, TDefault, TFlags>; nullable(msg?: Message): ObjectSchema; nonNullable( msg?: Message, ): ObjectSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): ObjectSchema>; strip( enabled?: true, ): ObjectSchema>; } export default class ObjectSchema< TIn extends Maybe, TContext = AnyObject, TDefault = any, TFlags extends Flags = '', > extends Schema, TContext, TDefault, TFlags> { fields: Shape, TContext> = Object.create(null); declare spec: ObjectSchemaSpec; private _sortErrors = defaultSort; private _nodes: string[] = []; private _excludedEdges: readonly [nodeA: string, nodeB: string][] = []; constructor(spec?: Shape) { super({ type: 'object', check(value): value is NonNullable> { return isObject(value) || typeof value === 'function'; }, }); this.withMutation(() => { if (spec) { this.shape(spec as any); } }); } protected _cast(_value: any, options: InternalOptions = {}) { let value = super._cast(_value, options); //should ignore nulls here if (value === undefined) return this.getDefault(options); if (!this._typeCheck(value)) return value; let fields = this.fields; let strip = options.stripUnknown ?? this.spec.noUnknown; let props = ([] as string[]).concat( this._nodes, Object.keys(value).filter((v) => !this._nodes.includes(v)), ); let intermediateValue: Record = {}; // is filled during the transform below let innerOptions: InternalOptions = { ...options, parent: intermediateValue, __validating: options.__validating || false, }; let isChanged = false; for (const prop of props) { let field = fields[prop]; let exists = prop in (value as {})!; let inputValue = value[prop]; if (field) { let fieldValue; // safe to mutate since this is fired in sequence innerOptions.path = (options.path ? `${options.path}.` : '') + prop; field = field.resolve({ value: inputValue, context: options.context, parent: intermediateValue, }); let fieldSpec = field instanceof Schema ? field.spec : undefined; let strict = fieldSpec?.strict; if (fieldSpec?.strip) { isChanged = isChanged || prop in (value as {}); continue; } fieldValue = !options.__validating || !strict ? (field as ISchema).cast(inputValue, innerOptions) : inputValue; if (fieldValue !== undefined) { intermediateValue[prop] = fieldValue; } } else if (exists && !strip) { intermediateValue[prop] = inputValue; } if ( exists !== prop in intermediateValue || intermediateValue[prop] !== inputValue ) { isChanged = true; } } return isChanged ? intermediateValue : value; } protected _validate( _value: any, options: InternalOptions = {}, panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ) { let { from = [], originalValue = _value, recursive = this.spec.recursive, } = options; options.from = [{ schema: this, value: originalValue }, ...from]; // this flag is needed for handling `strict` correctly in the context of // validation vs just casting. e.g strict() on a field is only used when validating options.__validating = true; options.originalValue = originalValue; super._validate(_value, options, panic, (objectErrors, value) => { if (!recursive || !isObject(value)) { next(objectErrors, value); return; } originalValue = originalValue || value; let tests = [] as Test[]; for (let key of this._nodes) { let field = this.fields[key]; if (!field || Reference.isRef(field)) { continue; } tests.push( field.asNestedTest({ options, key, parent: value, parentPath: options.path, originalParent: originalValue, }), ); } this.runTests( { tests, value, originalValue, options }, panic, (fieldErrors) => { next(fieldErrors.sort(this._sortErrors).concat(objectErrors), value); }, ); }); } clone(spec?: Partial): this { const next = super.clone(spec); next.fields = { ...this.fields }; next._nodes = this._nodes; next._excludedEdges = this._excludedEdges; next._sortErrors = this._sortErrors; return next; } concat, IC, ID, IF extends Flags>( schema: ObjectSchema, ): ObjectSchema< ConcatObjectTypes, TContext & IC, Extract extends never ? // this _attempts_ to cover the default from shape case TDefault extends AnyObject ? ID extends AnyObject ? _> : ID : ID : ID, TFlags | IF >; concat(schema: this): this; concat(schema: any): any { let next = super.concat(schema) as any; let nextFields = next.fields; for (let [field, schemaOrRef] of Object.entries(this.fields)) { const target = nextFields[field]; nextFields[field] = target === undefined ? schemaOrRef : target; } return next.withMutation((s: any) => // XXX: excludes here is wrong s.setFields(nextFields, [ ...this._excludedEdges, ...schema._excludedEdges, ]), ); } protected _getDefault(options?: ResolveOptions) { if ('default' in this.spec) { return super._getDefault(options); } // if there is no default set invent one if (!this._nodes.length) { return undefined; } let dft: any = {}; this._nodes.forEach((key) => { const field = this.fields[key] as any; let innerOptions = options; if (innerOptions?.value) { innerOptions = { ...innerOptions, parent: innerOptions.value, value: innerOptions.value[key], }; } dft[key] = field && 'getDefault' in field ? field.getDefault(innerOptions) : undefined; }); return dft; } private setFields, TDefaultNext>( shape: Shape, excludedEdges?: readonly [string, string][], ): ObjectSchema { let next = this.clone() as any; next.fields = shape; next._nodes = sortFields(shape, excludedEdges); next._sortErrors = sortByKeyOrder(Object.keys(shape)); // XXX: this carries over edges which may not be what you want if (excludedEdges) next._excludedEdges = excludedEdges; return next; } shape( additions: U, excludes: readonly [string, string][] = [], ) { type UIn = TypeFromShape; type UDefault = Extract extends never ? // not defaulted then assume the default is derived and should be merged _> : TDefault; return this.clone().withMutation((next) => { let edges = next._excludedEdges; if (excludes.length) { if (!Array.isArray(excludes[0])) excludes = [excludes as any]; edges = [...next._excludedEdges, ...excludes]; } // XXX: excludes here is wrong return next.setFields<_>, UDefault>( Object.assign(next.fields, additions) as any, edges, ); }); } partial() { const partial: any = {}; for (const [key, schema] of Object.entries(this.fields)) { partial[key] = 'optional' in schema && schema.optional instanceof Function ? schema.optional() : schema; } return this.setFields, TDefault>(partial); } deepPartial(): ObjectSchema, TContext, TDefault, TFlags> { const next = deepPartial(this); return next; } pick(keys: readonly TKey[]) { const picked: any = {}; for (const key of keys) { if (this.fields[key]) picked[key] = this.fields[key]; } return this.setFields<{ [K in TKey]: TIn[K] }, TDefault>( picked, this._excludedEdges.filter( ([a, b]) => keys.includes(a as TKey) && keys.includes(b as TKey), ), ); } omit(keys: readonly TKey[]) { const remaining: TKey[] = []; for (const key of Object.keys(this.fields) as TKey[]) { if (keys.includes(key)) continue; remaining.push(key); } return this.pick>(remaining as any); } from(from: string, to: keyof TIn, alias?: boolean) { let fromGetter = getter(from, true); return this.transform((obj) => { if (!obj) return obj; let newObj = obj; if (deepHas(obj, from)) { newObj = { ...obj }; if (!alias) delete newObj[from]; newObj[to] = fromGetter(obj); } return newObj; }); } /** Parse an input JSON string to an object */ json() { return this.transform(parseJson); } /** * Similar to `noUnknown` but only validates that an object is the right shape without stripping the unknown keys */ exact(message?: Message): this { return this.test({ name: 'exact', exclusive: true, message: message || locale.exact, test(value) { if (value == null) return true; const unknownKeys = unknown(this.schema, value); return ( unknownKeys.length === 0 || this.createError({ params: { properties: unknownKeys.join(', ') } }) ); }, }); } stripUnknown(): this { return this.clone({ noUnknown: true }); } noUnknown(message?: Message): this; noUnknown(noAllow: boolean, message?: Message): this; noUnknown(noAllow: Message | boolean = true, message = locale.noUnknown) { if (typeof noAllow !== 'boolean') { message = noAllow; noAllow = true; } let next = this.test({ name: 'noUnknown', exclusive: true, message: message, test(value) { if (value == null) return true; const unknownKeys = unknown(this.schema, value); return ( !noAllow || unknownKeys.length === 0 || this.createError({ params: { unknown: unknownKeys.join(', ') } }) ); }, }); next.spec.noUnknown = noAllow; return next; } unknown(allow = true, message = locale.noUnknown) { return this.noUnknown(!allow, message); } transformKeys(fn: (key: string) => string) { return this.transform((obj) => { if (!obj) return obj; const result: AnyObject = {}; for (const key of Object.keys(obj)) result[fn(key)] = obj[key]; return result; }); } camelCase() { return this.transformKeys(camelCase); } snakeCase() { return this.transformKeys(snakeCase); } constantCase() { return this.transformKeys((key) => snakeCase(key).toUpperCase()); } describe(options?: ResolveOptions) { const next = (options ? this.resolve(options) : this).clone(); const base = super.describe(options) as SchemaObjectDescription; base.fields = {}; for (const [key, value] of Object.entries(next.fields)) { let innerOptions = options; if (innerOptions?.value) { innerOptions = { ...innerOptions, parent: innerOptions.value, value: innerOptions.value[key], }; } base.fields[key] = value.describe(innerOptions); } return base; } } create.prototype = ObjectSchema.prototype; ================================================ FILE: src/schema.ts ================================================ import { mixed as locale } from './locale'; import Condition, { ConditionBuilder, ConditionConfig, ResolveOptions, } from './Condition'; import createValidation, { TestFunction, Test, TestConfig, NextCallback, PanicCallback, TestOptions, resolveParams, } from './util/createValidation'; import printValue from './util/printValue'; import Ref from './Reference'; import { getIn } from './util/reach'; import { ValidateOptions, TransformFunction, Message, InternalOptions, ExtraParams, ISchema, NestedTestConfig, DefaultThunk, } from './types'; import ValidationError from './ValidationError'; import ReferenceSet from './util/ReferenceSet'; import Reference from './Reference'; import isAbsent from './util/isAbsent'; import type { Flags, Maybe, ResolveFlags, _ } from './util/types'; import toArray from './util/toArray'; import cloneDeep from './util/cloneDeep'; import { issuesFromValidationError, StandardResult, type StandardSchema, type StandardSchemaProps, } from './standardSchema'; export type SchemaSpec = { coerce: boolean; nullable: boolean; optional: boolean; default?: TDefault | (() => TDefault); abortEarly?: boolean; strip?: boolean; strict?: boolean; recursive?: boolean; disableStackTrace?: boolean; label?: string | undefined; meta?: SchemaMetadata; }; export interface CustomSchemaMetadata {} // If `CustomSchemaMeta` isn't extended with any keys, we'll fall back to a // loose Record definition allowing free form usage. export type SchemaMetadata = keyof CustomSchemaMetadata extends never ? Record : CustomSchemaMetadata; export type SchemaOptions = { type: string; spec?: Partial>; check: (value: any) => value is NonNullable; }; export type AnySchema< TType = any, C = any, D = any, F extends Flags = Flags, > = Schema; export interface CastOptions { parent?: any; context?: C; assert?: boolean; stripUnknown?: boolean; // XXX: should be private? path?: string; index?: number; key?: string; originalValue?: any; value?: any; resolved?: boolean; } export interface CastOptionalityOptions extends Omit, 'assert'> { /** * Whether or not to throw TypeErrors if casting fails to produce a valid type. * defaults to `true`. The `'ignore-optionality'` options is provided as a migration * path from pre-v1 where `schema.nullable().required()` was allowed. When provided * cast will only throw for values that are the wrong type *not* including `null` and `undefined` */ assert: 'ignore-optionality'; } export type RunTest = ( opts: TestOptions, panic: PanicCallback, next: NextCallback, ) => void; export type TestRunOptions = { tests: RunTest[]; path?: string | undefined; options: InternalOptions; originalValue: any; value: any; }; export interface SchemaRefDescription { type: 'ref'; key: string; } export interface SchemaInnerTypeDescription extends SchemaDescription { innerType?: SchemaFieldDescription | SchemaFieldDescription[]; } export interface SchemaObjectDescription extends SchemaDescription { fields: Record; } export interface SchemaLazyDescription { type: string; label?: string; meta?: SchemaMetadata; } export type SchemaFieldDescription = | SchemaDescription | SchemaRefDescription | SchemaObjectDescription | SchemaInnerTypeDescription | SchemaLazyDescription; export interface SchemaDescription { type: string; label?: string; meta?: SchemaMetadata; oneOf: unknown[]; notOneOf: unknown[]; default?: unknown; nullable: boolean; optional: boolean; tests: Array<{ name?: string; params: ExtraParams | undefined }>; } export default abstract class Schema< TType = any, TContext = any, TDefault = any, TFlags extends Flags = '', > implements ISchema, StandardSchema> { readonly type: string; declare readonly __outputType: ResolveFlags; declare readonly __context: TContext; declare readonly __flags: TFlags; declare readonly __isYupSchema__: boolean; declare readonly __default: TDefault; readonly deps: readonly string[] = []; tests: Test[]; transforms: TransformFunction[]; private conditions: Condition[] = []; private _mutate?: boolean; private internalTests: Record = {}; protected _whitelist = new ReferenceSet(); protected _blacklist = new ReferenceSet(); protected exclusiveTests: Record = Object.create(null); protected _typeCheck: (value: any) => value is NonNullable; spec: SchemaSpec; constructor(options: SchemaOptions) { this.tests = []; this.transforms = []; this.withMutation(() => { this.typeError(locale.notType); }); this.type = options.type; this._typeCheck = options.check; this.spec = { strip: false, strict: false, abortEarly: true, recursive: true, disableStackTrace: false, nullable: false, optional: true, coerce: true, ...options?.spec, }; this.withMutation((s) => { s.nonNullable(); }); } // TODO: remove get _type() { return this.type; } clone(spec?: Partial>): this { if (this._mutate) { if (spec) Object.assign(this.spec, spec); return this; } // if the nested value is a schema we can skip cloning, since // they are already immutable const next: AnySchema = Object.create(Object.getPrototypeOf(this)); // @ts-expect-error this is readonly next.type = this.type; next._typeCheck = this._typeCheck; next._whitelist = this._whitelist.clone(); next._blacklist = this._blacklist.clone(); next.internalTests = { ...this.internalTests }; next.exclusiveTests = { ...this.exclusiveTests }; // @ts-expect-error this is readonly next.deps = [...this.deps]; next.conditions = [...this.conditions]; next.tests = [...this.tests]; next.transforms = [...this.transforms]; next.spec = cloneDeep({ ...this.spec, ...spec }); return next as this; } label(label: string) { let next = this.clone(); next.spec.label = label; return next; } meta(): SchemaMetadata | undefined; meta(obj: SchemaMetadata): this; meta(...args: [SchemaMetadata?]) { if (args.length === 0) return this.spec.meta; let next = this.clone(); next.spec.meta = Object.assign(next.spec.meta || {}, args[0]); return next; } withMutation(fn: (schema: this) => T): T { let before = this._mutate; this._mutate = true; let result = fn(this); this._mutate = before; return result; } concat(schema: this): this; concat(schema: AnySchema): AnySchema; concat(schema: AnySchema): AnySchema { if (!schema || schema === this) return this; if (schema.type !== this.type && this.type !== 'mixed') throw new TypeError( `You cannot \`concat()\` schema's of different types: ${this.type} and ${schema.type}`, ); let base = this; let combined = schema.clone(); const mergedSpec = { ...base.spec, ...combined.spec }; combined.spec = mergedSpec; combined.internalTests = { ...base.internalTests, ...combined.internalTests, }; // manually merge the blacklist/whitelist (the other `schema` takes // precedence in case of conflicts) combined._whitelist = base._whitelist.merge( schema._whitelist, schema._blacklist, ); combined._blacklist = base._blacklist.merge( schema._blacklist, schema._whitelist, ); // start with the current tests combined.tests = base.tests; combined.exclusiveTests = base.exclusiveTests; // manually add the new tests to ensure // the deduping logic is consistent combined.withMutation((next) => { schema.tests.forEach((fn) => { next.test(fn.OPTIONS!); }); }); combined.transforms = [...base.transforms, ...combined.transforms]; return combined as any; } isType(v: unknown): v is TType { if (v == null) { if (this.spec.nullable && v === null) return true; if (this.spec.optional && v === undefined) return true; return false; } return this._typeCheck(v); } resolve(options: ResolveOptions) { let schema = this; if (schema.conditions.length) { let conditions = schema.conditions; schema = schema.clone(); schema.conditions = []; schema = conditions.reduce( (prevSchema, condition) => condition.resolve(prevSchema, options) as any, schema, ) as any as this; schema = schema.resolve(options); } return schema; } protected resolveOptions>(options: T): T { return { ...options, from: options.from || [], strict: options.strict ?? this.spec.strict, abortEarly: options.abortEarly ?? this.spec.abortEarly, recursive: options.recursive ?? this.spec.recursive, disableStackTrace: options.disableStackTrace ?? this.spec.disableStackTrace, }; } /** * Run the configured transform pipeline over an input value. */ cast(value: any, options?: CastOptions): this['__outputType']; cast( value: any, options: CastOptionalityOptions, ): this['__outputType'] | null | undefined; cast( value: any, options: CastOptions | CastOptionalityOptions = {}, ): this['__outputType'] { let resolvedSchema = this.resolve({ ...options, value, // parent: options.parent, // context: options.context, }); let allowOptionality = options.assert === 'ignore-optionality'; let result = resolvedSchema._cast(value, options as any); if (options.assert !== false && !resolvedSchema.isType(result)) { if (allowOptionality && isAbsent(result)) { return result as any; } let formattedValue = printValue(value); let formattedResult = printValue(result); throw new TypeError( `The value of ${ options.path || 'field' } could not be cast to a value ` + `that satisfies the schema type: "${resolvedSchema.type}". \n\n` + `attempted value: ${formattedValue} \n` + (formattedResult !== formattedValue ? `result of cast: ${formattedResult}` : ''), ); } return result; } protected _cast(rawValue: any, options: CastOptions): any { let value = rawValue === undefined ? rawValue : this.transforms.reduce( (prevValue, fn) => fn.call(this, prevValue, rawValue, this, options), rawValue, ); if (value === undefined) { value = this.getDefault(options); } return value; } protected _validate( _value: any, options: InternalOptions = {}, panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ): void { let { path, originalValue = _value, strict = this.spec.strict } = options; let value = _value; if (!strict) { value = this._cast(value, { assert: false, ...options }); } let initialTests = []; for (let test of Object.values(this.internalTests)) { if (test) initialTests.push(test); } this.runTests( { path, value, originalValue, options, tests: initialTests, }, panic, (initialErrors) => { // even if we aren't ending early we can't proceed further if the types aren't correct if (initialErrors.length) { return next(initialErrors, value); } this.runTests( { path, value, originalValue, options, tests: this.tests, }, panic, next, ); }, ); } /** * Executes a set of validations, either schema, produced Tests or a nested * schema validate result. */ protected runTests( runOptions: TestRunOptions, panic: (err: Error, value: unknown) => void, next: (errors: ValidationError[], value: unknown) => void, ): void { let fired = false; let { tests, value, originalValue, path, options } = runOptions; let panicOnce = (arg: Error) => { if (fired) return; fired = true; panic(arg, value); }; let nextOnce = (arg: ValidationError[]) => { if (fired) return; fired = true; next(arg, value); }; let count = tests.length; let nestedErrors = [] as ValidationError[]; if (!count) return nextOnce([]); let args = { value, originalValue, path, options, schema: this, }; for (let i = 0; i < tests.length; i++) { const test = tests[i]; test(args!, panicOnce, function finishTestRun(err) { if (err) { Array.isArray(err) ? nestedErrors.push(...err) : nestedErrors.push(err); } if (--count <= 0) { nextOnce(nestedErrors); } }); } } asNestedTest({ key, index, parent, parentPath, originalParent, options, }: NestedTestConfig): RunTest { const k = key ?? index; if (k == null) { throw TypeError('Must include `key` or `index` for nested validations'); } const isIndex = typeof k === 'number'; let value = parent[k]; const testOptions = { ...options, // Nested validations fields are always strict: // 1. parent isn't strict so the casting will also have cast inner values // 2. parent is strict in which case the nested values weren't cast either strict: true, parent, value, originalValue: originalParent[k], // FIXME: tests depend on `index` being passed around deeply, // we should not let the options.key/index bleed through key: undefined, // index: undefined, [isIndex ? 'index' : 'key']: k, path: isIndex || k.includes('.') ? `${parentPath || ''}[${isIndex ? k : `"${k}"`}]` : (parentPath ? `${parentPath}.` : '') + key, }; return (_: any, panic, next) => this.resolve(testOptions)._validate(value, testOptions, panic, next); } validate( value: any, options?: ValidateOptions, ): Promise { let schema = this.resolve({ ...options, value }); let disableStackTrace = options?.disableStackTrace ?? schema.spec.disableStackTrace; return new Promise((resolve, reject) => schema._validate( value, options, (error, parsed) => { if (ValidationError.isError(error)) error.value = parsed; reject(error); }, (errors, validated) => { if (errors.length) reject( new ValidationError( errors!, validated, undefined, undefined, disableStackTrace, ), ); else resolve(validated as this['__outputType']); }, ), ); } validateSync( value: any, options?: ValidateOptions, ): this['__outputType'] { let schema = this.resolve({ ...options, value }); let result: any; let disableStackTrace = options?.disableStackTrace ?? schema.spec.disableStackTrace; schema._validate( value, { ...options, sync: true }, (error, parsed) => { if (ValidationError.isError(error)) error.value = parsed; throw error; }, (errors, validated) => { if (errors.length) throw new ValidationError( errors!, value, undefined, undefined, disableStackTrace, ); result = validated; }, ); return result; } isValid(value: any, options?: ValidateOptions): Promise { return this.validate(value, options).then( () => true, (err) => { if (ValidationError.isError(err)) return false; throw err; }, ); } isValidSync( value: any, options?: ValidateOptions, ): value is this['__outputType'] { try { this.validateSync(value, options); return true; } catch (err) { if (ValidationError.isError(err)) return false; throw err; } } protected _getDefault(options?: ResolveOptions) { let defaultValue = this.spec.default; if (defaultValue == null) { return defaultValue; } return typeof defaultValue === 'function' ? defaultValue.call(this, options) : cloneDeep(defaultValue); } getDefault( options?: ResolveOptions, // If schema is defaulted we know it's at least not undefined ): TDefault { let schema = this.resolve(options || {}); return schema._getDefault(options); } default(def: DefaultThunk): any { if (arguments.length === 0) { return this._getDefault(); } let next = this.clone({ default: def }); return next as any; } strict(isStrict = true) { return this.clone({ strict: isStrict }); } protected nullability(nullable: boolean, message?: Message) { const next = this.clone({ nullable }); next.internalTests.nullable = createValidation({ message, name: 'nullable', test(value) { return value === null ? this.schema.spec.nullable : true; }, }); return next; } protected optionality(optional: boolean, message?: Message) { const next = this.clone({ optional }); next.internalTests.optionality = createValidation({ message, name: 'optionality', test(value) { return value === undefined ? this.schema.spec.optional : true; }, }); return next; } optional(): any { return this.optionality(true); } defined(message = locale.defined): any { return this.optionality(false, message); } nullable(): any { return this.nullability(true); } nonNullable(message = locale.notNull): any { return this.nullability(false, message); } required(message: Message = locale.required): any { return this.clone().withMutation((next) => next.nonNullable(message).defined(message), ); } notRequired(): any { return this.clone().withMutation((next) => next.nullable().optional()); } transform(fn: TransformFunction) { let next = this.clone(); next.transforms.push(fn as TransformFunction); return next; } /** * Adds a test function to the schema's queue of tests. * tests can be exclusive or non-exclusive. * * - exclusive tests, will replace any existing tests of the same name. * - non-exclusive: can be stacked * * If a non-exclusive test is added to a schema with an exclusive test of the same name * the exclusive test is removed and further tests of the same name will be stacked. * * If an exclusive test is added to a schema with non-exclusive tests of the same name * the previous tests are removed and further tests of the same name will replace each other. */ test(options: TestConfig): this; test(test: TestFunction): this; test(name: string, test: TestFunction): this; test( name: string, message: Message, test: TestFunction, ): this; test(...args: any[]) { let opts: TestConfig; if (args.length === 1) { if (typeof args[0] === 'function') { opts = { test: args[0] }; } else { opts = args[0]; } } else if (args.length === 2) { opts = { name: args[0], test: args[1] }; } else { opts = { name: args[0], message: args[1], test: args[2] }; } if (opts.message === undefined) opts.message = locale.default; if (typeof opts.test !== 'function') throw new TypeError('`test` is a required parameters'); let next = this.clone(); let validate = createValidation(opts); let isExclusive = opts.exclusive || (opts.name && next.exclusiveTests[opts.name] === true); if (opts.exclusive) { if (!opts.name) throw new TypeError( 'Exclusive tests must provide a unique `name` identifying the test', ); } if (opts.name) next.exclusiveTests[opts.name] = !!opts.exclusive; next.tests = next.tests.filter((fn) => { if (fn.OPTIONS!.name === opts.name) { if (isExclusive) return false; if (fn.OPTIONS!.test === validate.OPTIONS.test) return false; } return true; }); next.tests.push(validate); return next; } when(builder: ConditionBuilder): this; when(keys: string | string[], builder: ConditionBuilder): this; when(options: ConditionConfig): this; when(keys: string | string[], options: ConditionConfig): this; when( keys: string | string[] | ConditionBuilder | ConditionConfig, options?: ConditionBuilder | ConditionConfig, ) { if (!Array.isArray(keys) && typeof keys !== 'string') { options = keys; keys = '.'; } let next = this.clone(); let deps = toArray(keys).map((key) => new Ref(key)); deps.forEach((dep) => { // @ts-ignore readonly array if (dep.isSibling) next.deps.push(dep.key); }); next.conditions.push( (typeof options === 'function' ? new Condition(deps, options!) : Condition.fromOptions(deps, options!)) as Condition, ); return next; } typeError(message: Message) { let next = this.clone(); next.internalTests.typeError = createValidation({ message, name: 'typeError', skipAbsent: true, test(value) { if (!this.schema._typeCheck(value)) return this.createError({ params: { type: this.schema.type, }, }); return true; }, }); return next; } oneOf( enums: ReadonlyArray, message?: Message<{ values: any }>, ): this; oneOf( enums: ReadonlyArray, message: Message<{ values: any }>, ): any; oneOf( enums: ReadonlyArray, message = locale.oneOf, ): any { let next = this.clone(); enums.forEach((val) => { next._whitelist.add(val); next._blacklist.delete(val); }); next.internalTests.whiteList = createValidation({ message, name: 'oneOf', skipAbsent: true, test(value) { let valids = (this.schema as Schema)._whitelist; let resolved = valids.resolveAll(this.resolve); return resolved.includes(value) ? true : this.createError({ params: { values: Array.from(valids).join(', '), resolved, }, }); }, }); return next; } notOneOf( enums: ReadonlyArray | Reference>, message = locale.notOneOf, ): this { let next = this.clone(); enums.forEach((val) => { next._blacklist.add(val); next._whitelist.delete(val); }); next.internalTests.blacklist = createValidation({ message, name: 'notOneOf', test(value) { let invalids = (this.schema as Schema)._blacklist; let resolved = invalids.resolveAll(this.resolve); if (resolved.includes(value)) return this.createError({ params: { values: Array.from(invalids).join(', '), resolved, }, }); return true; }, }); return next; } strip(strip = true): any { let next = this.clone(); next.spec.strip = strip; return next as any; } /** * Return a serialized description of the schema including validations, flags, types etc. * * @param options Provide any needed context for resolving runtime schema alterations (lazy, when conditions, etc). */ describe(options?: ResolveOptions) { const next = (options ? this.resolve(options) : this).clone(); const { label, meta, optional, nullable } = next.spec; const description: SchemaDescription = { meta, label, optional, nullable, default: next.getDefault(options), type: next.type, oneOf: next._whitelist.describe(), notOneOf: next._blacklist.describe(), tests: next.tests .filter( (n, idx, list) => list.findIndex((c) => c.OPTIONS!.name === n.OPTIONS!.name) === idx, ) .map((fn) => { const params = fn.OPTIONS!.params && options ? resolveParams({ ...fn.OPTIONS!.params }, options) : fn.OPTIONS!.params; return { name: fn.OPTIONS!.name, params, }; }), }; return description; } get ['~standard']() { const schema = this; const standard: StandardSchemaProps< TType, ResolveFlags > = { version: 1, vendor: 'yup', async validate( value: unknown, ): Promise>> { try { const result = await schema.validate(value, { abortEarly: false, }); return { value: result as ResolveFlags, }; } catch (err) { if (err instanceof ValidationError) { return { issues: issuesFromValidationError(err), }; } throw err; } }, }; return standard; } } export default interface Schema< /* eslint-disable @typescript-eslint/no-unused-vars */ TType = any, TContext = any, TDefault = any, TFlags extends Flags = '', /* eslint-enable @typescript-eslint/no-unused-vars */ > { validateAt( path: string, value: any, options?: ValidateOptions, ): Promise; validateSyncAt( path: string, value: any, options?: ValidateOptions, ): any; equals: Schema['oneOf']; is: Schema['oneOf']; not: Schema['notOneOf']; nope: Schema['notOneOf']; } // @ts-expect-error Schema.prototype.__isYupSchema__ = true; for (const method of ['validate', 'validateSync']) Schema.prototype[`${method}At` as 'validateAt' | 'validateSyncAt'] = function (path: string, value: any, options: ValidateOptions = {}) { const { parent, parentPath, schema } = getIn( this, path, value, options.context, ); return (schema as any)[method](parent && parent[parentPath], { ...options, parent, path, }); }; for (const alias of ['equals', 'is'] as const) Schema.prototype[alias] = Schema.prototype.oneOf; for (const alias of ['not', 'nope'] as const) Schema.prototype[alias] = Schema.prototype.notOneOf; ================================================ FILE: src/setLocale.ts ================================================ import locale, { LocaleObject } from './locale'; export type { LocaleObject }; export default function setLocale(custom: LocaleObject) { Object.keys(custom).forEach((type) => { // @ts-ignore Object.keys(custom[type]!).forEach((method) => { // @ts-ignore locale[type][method] = custom[type][method]; }); }); } ================================================ FILE: src/standardSchema.ts ================================================ /** * Copied from @standard-schema/spec to avoid having a dependency on it. * https://github.com/standard-schema/standard-schema/blob/main/packages/spec/src/index.ts */ import ValidationError from './ValidationError'; export interface StandardSchema { readonly '~standard': StandardSchemaProps; } export interface StandardSchemaProps { readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, ) => StandardResult | Promise>; readonly types?: StandardTypes | undefined; } export type StandardResult = | StandardSuccessResult | StandardFailureResult; export interface StandardSuccessResult { readonly value: Output; readonly issues?: undefined; } export interface StandardFailureResult { readonly issues: ReadonlyArray; } export interface StandardIssue { readonly message: string; readonly path?: ReadonlyArray | undefined; } export interface StandardPathSegment { readonly key: PropertyKey; } export interface StandardTypes { readonly input: Input; readonly output: Output; } export function createStandardPath( path: string | undefined, ): StandardIssue['path'] { if (!path?.length) { return undefined; } // Array to store the final path segments const segments: string[] = []; // Buffer for building the current segment let currentSegment = ''; // Track if we're inside square brackets (array/property access) let inBrackets = false; // Track if we're inside quotes (for property names with special chars) let inQuotes = false; for (let i = 0; i < path.length; i++) { const char = path[i]; if (char === '[' && !inQuotes) { // When entering brackets, push any accumulated segment after splitting on dots if (currentSegment) { segments.push(...currentSegment.split('.').filter(Boolean)); currentSegment = ''; } inBrackets = true; continue; } if (char === ']' && !inQuotes) { if (currentSegment) { // Handle numeric indices (e.g. arr[0]) if (/^\d+$/.test(currentSegment)) { segments.push(currentSegment); } else { // Handle quoted property names (e.g. obj["foo.bar"]) segments.push(currentSegment.replace(/^"|"$/g, '')); } currentSegment = ''; } inBrackets = false; continue; } if (char === '"') { // Toggle quote state for handling quoted property names inQuotes = !inQuotes; continue; } if (char === '.' && !inBrackets && !inQuotes) { // On dots outside brackets/quotes, push current segment if (currentSegment) { segments.push(currentSegment); currentSegment = ''; } continue; } currentSegment += char; } // Push any remaining segment after splitting on dots if (currentSegment) { segments.push(...currentSegment.split('.').filter(Boolean)); } return segments; } export function createStandardIssues( error: ValidationError, parentPath?: string, ): StandardIssue[] { const path = parentPath ? `${parentPath}.${error.path}` : error.path; return error.errors.map( (err) => ({ message: err, path: createStandardPath(path), } satisfies StandardIssue), ); } export function issuesFromValidationError( error: ValidationError, parentPath?: string, ): StandardIssue[] { if (!error.inner?.length && error.errors.length) { return createStandardIssues(error, parentPath); } const path = parentPath ? `${parentPath}.${error.path}` : error.path; return error.inner.flatMap((err) => issuesFromValidationError(err, path)); } ================================================ FILE: src/string.ts ================================================ import { MixedLocale, mixed as mixedLocale, string as locale } from './locale'; import isAbsent from './util/isAbsent'; import type Reference from './Reference'; import type { Message, AnyObject, DefaultThunk } from './types'; import type { Concat, Defined, Flags, NotNull, SetFlag, ToggleDefault, UnsetFlag, Maybe, Optionals, } from './util/types'; import Schema from './schema'; import { parseDateStruct } from './util/parseIsoDate'; // Taken from HTML spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address let rEmail = // eslint-disable-next-line /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; let rUrl = // eslint-disable-next-line /^((https?|ftp):)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; // eslint-disable-next-line let rUUID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; let yearMonthDay = '^\\d{4}-\\d{2}-\\d{2}'; let hourMinuteSecond = '\\d{2}:\\d{2}:\\d{2}'; let zOrOffset = '(([+-]\\d{2}(:?\\d{2})?)|Z)'; let rIsoDateTime = new RegExp( `${yearMonthDay}T${hourMinuteSecond}(\\.\\d+)?${zOrOffset}$`, ); let isTrimmed = (value: Maybe) => isAbsent(value) || value === value.trim(); export type MatchOptions = { excludeEmptyString?: boolean; message: Message<{ regex: RegExp }>; name?: string; }; export type DateTimeOptions = { message: Message<{ allowOffset?: boolean; precision?: number }>; /** Allow a time zone offset. False requires UTC 'Z' timezone. (default: false) */ allowOffset?: boolean; /** Require a certain sub-second precision on the date. (default: undefined -- any or no sub-second precision) */ precision?: number; }; let objStringTag = {}.toString(); function create(): StringSchema; function create< T extends string, TContext extends Maybe = AnyObject, >(): StringSchema; function create() { return new StringSchema(); } export { create }; export default class StringSchema< TType extends Maybe = string | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { constructor() { super({ type: 'string', check(value): value is NonNullable { if (value instanceof String) value = value.valueOf(); return typeof value === 'string'; }, }); this.withMutation(() => { this.transform((value, _raw) => { if (!this.spec.coerce || this.isType(value)) return value; // don't ever convert arrays if (Array.isArray(value)) return value; const strValue = value != null && value.toString ? value.toString() : value; // no one wants plain objects converted to [Object object] if (strValue === objStringTag) return value; return strValue; }); }); } required(message?: Message) { return super.required(message).withMutation((schema: this) => schema.test({ message: message || mixedLocale.required, name: 'required', skipAbsent: true, test: (value) => !!value!.length, }), ); } notRequired() { return super.notRequired().withMutation((schema: this) => { schema.tests = schema.tests.filter((t) => t.OPTIONS!.name !== 'required'); return schema; }); } length( length: number | Reference, message: Message<{ length: number }> = locale.length, ) { return this.test({ message, name: 'length', exclusive: true, params: { length }, skipAbsent: true, test(value: Maybe) { return value!.length === this.resolve(length); }, }); } min( min: number | Reference, message: Message<{ min: number }> = locale.min, ) { return this.test({ message, name: 'min', exclusive: true, params: { min }, skipAbsent: true, test(value: Maybe) { return value!.length >= this.resolve(min); }, }); } max( max: number | Reference, message: Message<{ max: number }> = locale.max, ) { return this.test({ name: 'max', exclusive: true, message, params: { max }, skipAbsent: true, test(value: Maybe) { return value!.length <= this.resolve(max); }, }); } matches(regex: RegExp, options?: MatchOptions | MatchOptions['message']) { let excludeEmptyString = false; let message; let name; if (options) { if (typeof options === 'object') { ({ excludeEmptyString = false, message, name, } = options as MatchOptions); } else { message = options; } } return this.test({ name: name || 'matches', message: message || locale.matches, params: { regex }, skipAbsent: true, test: (value: Maybe) => (value === '' && excludeEmptyString) || value!.search(regex) !== -1, }); } email(message = locale.email) { return this.matches(rEmail, { name: 'email', message, excludeEmptyString: true, }); } url(message = locale.url) { return this.matches(rUrl, { name: 'url', message, excludeEmptyString: true, }); } uuid(message = locale.uuid) { return this.matches(rUUID, { name: 'uuid', message, excludeEmptyString: false, }); } datetime(options?: DateTimeOptions | DateTimeOptions['message']) { let message: DateTimeOptions['message'] = ''; let allowOffset: DateTimeOptions['allowOffset']; let precision: DateTimeOptions['precision']; if (options) { if (typeof options === 'object') { ({ message = '', allowOffset = false, precision = undefined, } = options as DateTimeOptions); } else { message = options; } } return this.matches(rIsoDateTime, { name: 'datetime', message: message || locale.datetime, excludeEmptyString: true, }) .test({ name: 'datetime_offset', message: message || locale.datetime_offset, params: { allowOffset }, skipAbsent: true, test: (value: Maybe) => { if (!value || allowOffset) return true; const struct = parseDateStruct(value); if (!struct) return false; return !!struct.z; }, }) .test({ name: 'datetime_precision', message: message || locale.datetime_precision, params: { precision }, skipAbsent: true, test: (value: Maybe) => { if (!value || precision == undefined) return true; const struct = parseDateStruct(value); if (!struct) return false; return struct.precision === precision; }, }); } //-- transforms -- ensure(): StringSchema> { return this.default('' as Defined).transform((val) => val === null ? '' : val, ) as any; } trim(message = locale.trim) { return this.transform((val) => (val != null ? val.trim() : val)).test({ message, name: 'trim', test: isTrimmed, }); } lowercase(message = locale.lowercase) { return this.transform((value) => !isAbsent(value) ? value.toLowerCase() : value, ).test({ message, name: 'string_case', exclusive: true, skipAbsent: true, test: (value: Maybe) => isAbsent(value) || value === value.toLowerCase(), }); } uppercase(message = locale.uppercase) { return this.transform((value) => !isAbsent(value) ? value.toUpperCase() : value, ).test({ message, name: 'string_case', exclusive: true, skipAbsent: true, test: (value: Maybe) => isAbsent(value) || value === value.toUpperCase(), }); } } create.prototype = StringSchema.prototype; // // String Interfaces // export default interface StringSchema< TType extends Maybe = string | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): StringSchema>; oneOf( arrayOfValues: ReadonlyArray>, message?: MixedLocale['oneOf'], ): StringSchema, TContext, TDefault, TFlags>; oneOf( enums: ReadonlyArray, message?: Message<{ values: any }>, ): this; concat, UContext, UDefault, UFlags extends Flags>( schema: StringSchema, ): StringSchema< Concat, TContext & UContext, UDefault, TFlags | UFlags >; concat(schema: this): this; defined( msg?: Message, ): StringSchema, TContext, TDefault, TFlags>; optional(): StringSchema; required( msg?: Message, ): StringSchema, TContext, TDefault, TFlags>; notRequired(): StringSchema, TContext, TDefault, TFlags>; nullable( msg?: Message, ): StringSchema; nonNullable( msg?: Message ): StringSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): StringSchema>; strip( enabled?: true, ): StringSchema>; } ================================================ FILE: src/tuple.ts ================================================ // @ts-ignore import type { AnyObject, DefaultThunk, InternalOptions, ISchema, Message, } from './types'; import type { Defined, Flags, NotNull, SetFlag, ToggleDefault, UnsetFlag, Maybe, } from './util/types'; import type { ResolveOptions } from './Condition'; import Schema, { RunTest, SchemaInnerTypeDescription, SchemaSpec, } from './schema'; import ValidationError from './ValidationError'; import { tuple as tupleLocale } from './locale'; type AnyTuple = [unknown, ...unknown[]]; export function create(schemas: { [K in keyof T]: ISchema; }) { return new TupleSchema(schemas); } export default interface TupleSchema< TType extends Maybe = AnyTuple | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { default>( def: DefaultThunk, ): TupleSchema>; concat>(schema: TOther): TOther; defined( msg?: Message, ): TupleSchema, TContext, TDefault, TFlags>; optional(): TupleSchema; required( msg?: Message, ): TupleSchema, TContext, TDefault, TFlags>; notRequired(): TupleSchema, TContext, TDefault, TFlags>; nullable( msg?: Message, ): TupleSchema; nonNullable( msg?: Message ): TupleSchema, TContext, TDefault, TFlags>; strip( enabled: false, ): TupleSchema>; strip( enabled?: true, ): TupleSchema>; } interface TupleSchemaSpec extends SchemaSpec { types: T extends any[] ? { [K in keyof T]: ISchema; } : never; } export default class TupleSchema< TType extends Maybe = AnyTuple | undefined, TContext = AnyObject, TDefault = undefined, TFlags extends Flags = '', > extends Schema { declare spec: TupleSchemaSpec; constructor(schemas: [ISchema, ...ISchema[]]) { super({ type: 'tuple', spec: { types: schemas } as any, check(v: any): v is NonNullable { const types = (this.spec as TupleSchemaSpec).types; return Array.isArray(v) && v.length === types.length; }, }); this.withMutation(() => { this.typeError(tupleLocale.notType); }); } protected _cast(inputValue: any, options: InternalOptions) { const { types } = this.spec; const value = super._cast(inputValue, options); if (!this._typeCheck(value)) { return value; } let isChanged = false; const castArray = types.map((type, idx) => { const castElement = type.cast(value[idx], { ...options, path: `${options.path || ''}[${idx}]`, parent: value, originalValue: value[idx], value: value[idx], index: idx, }); if (castElement !== value[idx]) isChanged = true; return castElement; }); return isChanged ? castArray : value; } protected _validate( _value: any, options: InternalOptions = {}, panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ) { let itemTypes = this.spec.types; super._validate(_value, options, panic, (tupleErrors, value) => { // intentionally not respecting recursive if (!this._typeCheck(value)) { next(tupleErrors, value); return; } let tests: RunTest[] = []; for (let [index, itemSchema] of itemTypes.entries()) { tests[index] = itemSchema!.asNestedTest({ options, index, parent: value, parentPath: options.path, originalParent: options.originalValue ?? _value, }); } this.runTests( { value, tests, originalValue: options.originalValue ?? _value, options, }, panic, (innerTypeErrors) => next(innerTypeErrors.concat(tupleErrors), value), ); }); } describe(options?: ResolveOptions) { const next = (options ? this.resolve(options) : this).clone(); const base = super.describe(options) as SchemaInnerTypeDescription; base.innerType = next.spec.types.map((schema, index) => { let innerOptions = options; if (innerOptions?.value) { innerOptions = { ...innerOptions, parent: innerOptions.value, value: innerOptions.value[index], }; } return schema.describe(innerOptions); }); return base; } } create.prototype = TupleSchema.prototype; ================================================ FILE: src/types.ts ================================================ import type { ResolveOptions } from './Condition'; import type { AnySchema, CastOptionalityOptions, CastOptions, SchemaFieldDescription, SchemaSpec, } from './schema'; import type { Test } from './util/createValidation'; import type { AnyObject } from './util/objectTypes'; import type { Flags } from './util/types'; export type { AnyObject, AnySchema }; export interface ISchema { __flags: F; __context: C; __outputType: T; __default: D; cast(value: any, options?: CastOptions): T; cast(value: any, options: CastOptionalityOptions): T | null | undefined; validate(value: any, options?: ValidateOptions): Promise; asNestedTest(config: NestedTestConfig): Test; describe(options?: ResolveOptions): SchemaFieldDescription; resolve(options: ResolveOptions): ISchema; } export type DefaultThunk = T | ((options?: ResolveOptions) => T); export type InferType> = T['__outputType']; export type TransformFunction = ( this: T, value: any, originalValue: any, schema: T, options: CastOptions, ) => any; export interface Ancester { schema: ISchema; value: any; } export interface ValidateOptions { /** * Only validate the input, skipping type casting and transformation. Default - false */ strict?: boolean; /** * Return from validation methods on the first error rather than after all validations run. Default - true */ abortEarly?: boolean; /** * Remove unspecified keys from objects. Default - false */ stripUnknown?: boolean; /** * When false validations will not descend into nested schema (relevant for objects or arrays). Default - true */ recursive?: boolean; /** * When true ValidationError instance won't include stack trace information. Default - false */ disableStackTrace?: boolean; /** * Any context needed for validating schema conditions (see: when()) */ context?: TContext; } export interface InternalOptions extends ValidateOptions { __validating?: boolean; originalValue?: any; index?: number; key?: string; parent?: any; path?: string; sync?: boolean; from?: Ancester[]; } export interface MessageParams { path: string; value: any; originalValue: any; originalPath: string; label: string; type: string; spec: SchemaSpec & Record; } export type Message = any> = | string | ((params: Extra & MessageParams) => unknown) | Record; export type ExtraParams = Record; export type AnyMessageParams = MessageParams & ExtraParams; export interface NestedTestConfig { options: InternalOptions; parent: any; originalParent: any; parentPath: string | undefined; key?: string; index?: number; } ================================================ FILE: src/util/ReferenceSet.ts ================================================ import type { SchemaRefDescription } from '../schema'; import Reference from '../Reference'; export default class ReferenceSet extends Set { describe() { const description = [] as Array; for (const item of this.values()) { description.push(Reference.isRef(item) ? item.describe() : item); } return description; } resolveAll(resolve: (v: unknown | Reference) => unknown) { let result = [] as unknown[]; for (const item of this.values()) { result.push(resolve(item)); } return result; } clone() { return new ReferenceSet(this.values()); } merge(newItems: ReferenceSet, removeItems: ReferenceSet) { const next = this.clone(); newItems.forEach((value) => next.add(value)); removeItems.forEach((value) => next.delete(value)); return next; } } ================================================ FILE: src/util/cloneDeep.ts ================================================ // tweaked from https://github.com/Kelin2025/nanoclone/blob/0abeb7635bda9b68ef2277093f76dbe3bf3948e1/src/index.js // MIT licensed import isSchema from './isSchema'; function clone(src: unknown, seen: Map = new Map()) { if (isSchema(src) || !src || typeof src !== 'object') return src; if (seen.has(src)) return seen.get(src); let copy: any; if (src instanceof Date) { // Date copy = new Date(src.getTime()); seen.set(src, copy); } else if (src instanceof RegExp) { // RegExp copy = new RegExp(src); seen.set(src, copy); } else if (Array.isArray(src)) { // Array copy = new Array(src.length); seen.set(src, copy); for (let i = 0; i < src.length; i++) copy[i] = clone(src[i], seen); } else if (src instanceof Map) { // Map copy = new Map(); seen.set(src, copy); for (const [k, v] of src.entries()) copy.set(k, clone(v, seen)); } else if (src instanceof Set) { // Set copy = new Set(); seen.set(src, copy); for (const v of src) copy.add(clone(v, seen)); } else if (src instanceof Object) { // Object copy = {}; seen.set(src, copy); for (const [k, v] of Object.entries(src)) copy[k] = clone(v, seen); } else { throw Error(`Unable to clone ${src}`); } return copy; } export default clone; ================================================ FILE: src/util/createValidation.ts ================================================ import ValidationError from '../ValidationError'; import Ref from '../Reference'; import { ValidateOptions, Message, InternalOptions, ExtraParams, ISchema, } from '../types'; import Reference from '../Reference'; import type { AnySchema } from '../schema'; import isAbsent from './isAbsent'; import { ResolveOptions } from '../Condition'; export type PanicCallback = (err: Error) => void; export type NextCallback = ( err: ValidationError[] | ValidationError | null, ) => void; export type CreateErrorOptions = { path?: string; message?: Message; params?: ExtraParams; type?: string; disableStackTrace?: boolean; }; export type TestContext = { path: string; options: ValidateOptions; originalValue: any; parent: any; from?: Array<{ schema: ISchema; value: any }>; schema: any; resolve: (value: T | Reference) => T; createError: (params?: CreateErrorOptions) => ValidationError; }; export type TestFunction = ( this: TestContext, value: T, context: TestContext, ) => void | boolean | ValidationError | Promise; export type TestOptions = { value: any; path?: string; options: InternalOptions; originalValue: any; schema: TSchema; }; export type TestConfig = { name?: string; message?: Message; test: TestFunction; params?: ExtraParams; exclusive?: boolean; skipAbsent?: boolean; }; export type Test = (( opts: TestOptions, panic: PanicCallback, next: NextCallback, ) => void) & { OPTIONS?: TestConfig; }; export default function createValidation(config: { name?: string; test: TestFunction; params?: ExtraParams; message?: Message; skipAbsent?: boolean; }) { function validate( { value, path = '', options, originalValue, schema }: TestOptions, panic: PanicCallback, next: NextCallback, ) { const { name, test, params, message, skipAbsent } = config; let { parent, context, abortEarly = schema.spec.abortEarly, disableStackTrace = schema.spec.disableStackTrace, } = options; const resolveOptions = { value, parent, context }; function createError(overrides: CreateErrorOptions = {}) { const nextParams = resolveParams( { value, originalValue, label: schema.spec.label, path: overrides.path || path, spec: schema.spec, disableStackTrace: overrides.disableStackTrace || disableStackTrace, ...params, ...overrides.params, }, resolveOptions, ); const error = new ValidationError( ValidationError.formatError(overrides.message || message, nextParams), value, nextParams.path, overrides.type || name, nextParams.disableStackTrace, ); error.params = nextParams; return error; } const invalid = abortEarly ? panic : next; let ctx = { path, parent, type: name, from: options.from, createError, resolve(item: T | Reference) { return resolveMaybeRef(item, resolveOptions); }, options, originalValue, schema, }; const handleResult = (validOrError: ReturnType) => { if (ValidationError.isError(validOrError)) invalid(validOrError); else if (!validOrError) invalid(createError()); else next(null); }; const handleError = (err: any) => { if (ValidationError.isError(err)) invalid(err); else panic(err); }; const shouldSkip = skipAbsent && isAbsent(value); if (shouldSkip) { return handleResult(true); } let result: ReturnType; try { result = test.call(ctx, value, ctx); if (typeof (result as any)?.then === 'function') { if (options.sync) { throw new Error( `Validation test of type: "${ctx.type}" returned a Promise during a synchronous validate. ` + `This test will finish after the validate call has returned`, ); } return Promise.resolve(result).then(handleResult, handleError); } } catch (err: any) { handleError(err); return; } handleResult(result); } validate.OPTIONS = config; return validate; } // Warning: mutates the input export function resolveParams( params: T, options: ResolveOptions, ) { if (!params) return params; type Keys = (keyof typeof params)[]; for (const key of Object.keys(params) as Keys) { params[key] = resolveMaybeRef(params[key], options); } return params; } function resolveMaybeRef(item: T | Reference, options: ResolveOptions) { return Ref.isRef(item) ? item.getValue(options.value, options.parent, options.context) : item; } ================================================ FILE: src/util/isAbsent.ts ================================================ const isAbsent = (value: any): value is undefined | null => value == null; export default isAbsent; ================================================ FILE: src/util/isSchema.ts ================================================ import type { ISchema } from '../types'; const isSchema = (obj: any): obj is ISchema => obj && obj.__isYupSchema__; export default isSchema; ================================================ FILE: src/util/objectTypes.ts ================================================ import type { Maybe, Optionals } from './types'; import type Reference from '../Reference'; import type { ISchema } from '../types'; export type ObjectShape = { [k: string]: ISchema | Reference }; export type AnyObject = { [k: string]: any }; export type ResolveStrip> = T extends ISchema< any, any, infer F > ? Extract extends never ? T['__outputType'] : never : T['__outputType']; export type TypeFromShape = { [K in keyof S]: S[K] extends ISchema ? ResolveStrip : S[K] extends Reference ? T : unknown; }; export type DefaultFromShape = { [K in keyof Shape]: Shape[K] extends ISchema ? Shape[K]['__default'] : undefined; }; export type MergeObjectTypes, U extends AnyObject> = | ({ [P in keyof T]: P extends keyof U ? U[P] : T[P] } & U) | Optionals; export type ConcatObjectTypes< T extends Maybe, U extends Maybe, > = | ({ [P in keyof T]: P extends keyof NonNullable ? NonNullable[P] : T[P]; } & U) | Optionals; export type PartialDeep = T extends | string | number | bigint | boolean | null | undefined | symbol | Date ? T | undefined : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray : { [K in keyof T]?: PartialDeep }; type OptionalKeys = { [k in keyof T]: undefined extends T[k] ? k : never; }[keyof T]; type RequiredKeys = Exclude>; export type MakePartial = { [k in OptionalKeys as T[k] extends never ? never : k]?: T[k]; } & { [k in RequiredKeys as T[k] extends never ? never : k]: T[k] }; ================================================ FILE: src/util/parseIsoDate.ts ================================================ /** * This file is a modified version of the file from the following repository: * Date.parse with progressive enhancement for ISO 8601 * NON-CONFORMANT EDITION. * © 2011 Colin Snover * Released under MIT license. */ // prettier-ignore // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm const isoReg = /^(\d{4}|[+-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2}):?(\d{2})(?::?(\d{2})(?:[,.](\d{1,}))?)?(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?)?$/; export function parseIsoDate(date: string): number { const struct = parseDateStruct(date); if (!struct) return Date.parse ? Date.parse(date) : Number.NaN; // timestamps without timezone identifiers should be considered local time if (struct.z === undefined && struct.plusMinus === undefined) { return new Date( struct.year, struct.month, struct.day, struct.hour, struct.minute, struct.second, struct.millisecond, ).valueOf(); } let totalMinutesOffset = 0; if (struct.z !== 'Z' && struct.plusMinus !== undefined) { totalMinutesOffset = struct.hourOffset * 60 + struct.minuteOffset; if (struct.plusMinus === '+') totalMinutesOffset = 0 - totalMinutesOffset; } return Date.UTC( struct.year, struct.month, struct.day, struct.hour, struct.minute + totalMinutesOffset, struct.second, struct.millisecond, ); } export function parseDateStruct(date: string) { const regexResult = isoReg.exec(date); if (!regexResult) return null; // use of toNumber() avoids NaN timestamps caused by “undefined” // values being passed to Date constructor return { year: toNumber(regexResult[1]), month: toNumber(regexResult[2], 1) - 1, day: toNumber(regexResult[3], 1), hour: toNumber(regexResult[4]), minute: toNumber(regexResult[5]), second: toNumber(regexResult[6]), millisecond: regexResult[7] ? // allow arbitrary sub-second precision beyond milliseconds toNumber(regexResult[7].substring(0, 3)) : 0, precision: regexResult[7]?.length ?? undefined, z: regexResult[8] || undefined, plusMinus: regexResult[9] || undefined, hourOffset: toNumber(regexResult[10]), minuteOffset: toNumber(regexResult[11]), }; } function toNumber(str: string, defaultValue = 0) { return Number(str) || defaultValue; } ================================================ FILE: src/util/parseJson.ts ================================================ import type { AnySchema, TransformFunction } from '../types'; const parseJson: TransformFunction = (value, _, schema: AnySchema) => { if (typeof value !== 'string') { return value; } let parsed = value; try { parsed = JSON.parse(value); } catch (err) { /* */ } return schema.isType(parsed) ? parsed : value; }; export default parseJson; ================================================ FILE: src/util/printValue.ts ================================================ const toString = Object.prototype.toString; const errorToString = Error.prototype.toString; const regExpToString = RegExp.prototype.toString; const symbolToString = typeof Symbol !== 'undefined' ? Symbol.prototype.toString : () => ''; const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/; function printNumber(val: any) { if (val != +val) return 'NaN'; const isNegativeZero = val === 0 && 1 / val < 0; return isNegativeZero ? '-0' : '' + val; } function printSimpleValue(val: any, quoteStrings = false) { if (val == null || val === true || val === false) return '' + val; const typeOf = typeof val; if (typeOf === 'number') return printNumber(val); if (typeOf === 'string') return quoteStrings ? `"${val}"` : val; if (typeOf === 'function') return '[Function ' + (val.name || 'anonymous') + ']'; if (typeOf === 'symbol') return symbolToString.call(val).replace(SYMBOL_REGEXP, 'Symbol($1)'); const tag = toString.call(val).slice(8, -1); if (tag === 'Date') return isNaN(val.getTime()) ? '' + val : val.toISOString(val); if (tag === 'Error' || val instanceof Error) return '[' + errorToString.call(val) + ']'; if (tag === 'RegExp') return regExpToString.call(val); return null; } export default function printValue(value: any, quoteStrings?: boolean) { let result = printSimpleValue(value, quoteStrings); if (result !== null) return result; return JSON.stringify( value, function (key, value) { let result = printSimpleValue(this[key], quoteStrings); if (result !== null) return result; return value; }, 2, ); } ================================================ FILE: src/util/reach.ts ================================================ import { forEach } from 'property-expr'; import type Reference from '../Reference'; import type { InferType, ISchema } from '../types'; import type { Get } from 'type-fest'; export function getIn( schema: any, path: string, value?: any, context: C = value, ): { schema: ISchema | Reference; parent: any; parentPath: string; } { let parent: any, lastPart: string, lastPartDebug: string; // root path: '' if (!path) return { parent, parentPath: path, schema }; forEach(path, (_part, isBracket, isArray) => { let part = isBracket ? _part.slice(1, _part.length - 1) : _part; schema = schema.resolve({ context, parent, value }); let isTuple = schema.type === 'tuple'; let idx = isArray ? parseInt(part, 10) : 0; if (schema.innerType || isTuple) { if (isTuple && !isArray) throw new Error( `Yup.reach cannot implicitly index into a tuple type. the path part "${lastPartDebug}" must contain an index to the tuple element, e.g. "${lastPartDebug}[0]"`, ); if (value && idx >= value.length) { throw new Error( `Yup.reach cannot resolve an array item at index: ${_part}, in the path: ${path}. ` + `because there is no value at that index. `, ); } parent = value; value = value && value[idx]; schema = isTuple ? schema.spec.types[idx] : schema.innerType!; } // sometimes the array index part of a path doesn't exist: "nested.arr.child" // in these cases the current part is the next schema and should be processed // in this iteration. For cases where the index signature is included this // check will fail and we'll handle the `child` part on the next iteration like normal if (!isArray) { if (!schema.fields || !schema.fields[part]) throw new Error( `The schema does not contain the path: ${path}. ` + `(failed at: ${lastPartDebug} which is a type: "${schema.type}")`, ); parent = value; value = value && value[part]; schema = schema.fields[part]; } lastPart = part; lastPartDebug = isBracket ? '[' + _part + ']' : '.' + _part; }); return { schema, parent, parentPath: lastPart! }; } function reach

>( obj: S, path: P, value?: any, context?: any, ): | Reference, P>> | ISchema, P>, S['__context']> { return getIn(obj, path, value, context).schema as any; } export default reach; ================================================ FILE: src/util/sortByKeyOrder.ts ================================================ import ValidationError from '../ValidationError'; function findIndex(arr: readonly string[], err: ValidationError) { let idx = Infinity; arr.some((key, ii) => { if (err.path?.includes(key)) { idx = ii; return true; } }); return idx; } export default function sortByKeyOrder(keys: readonly string[]) { return (a: ValidationError, b: ValidationError) => { return findIndex(keys, a) - findIndex(keys, b); }; } ================================================ FILE: src/util/sortFields.ts ================================================ // @ts-expect-error import toposort from 'toposort'; import { split } from 'property-expr'; import Ref from '../Reference'; import isSchema from './isSchema'; import { ObjectShape } from './objectTypes'; export default function sortFields( fields: ObjectShape, excludedEdges: readonly [string, string][] = [], ) { let edges = [] as Array<[string, string]>; let nodes = new Set(); let excludes = new Set(excludedEdges.map(([a, b]) => `${a}-${b}`)); function addNode(depPath: string, key: string) { let node = split(depPath)[0]; nodes.add(node); if (!excludes.has(`${key}-${node}`)) edges.push([key, node]); } for (const key of Object.keys(fields)) { let value = fields[key]; nodes.add(key); if (Ref.isRef(value) && value.isSibling) addNode(value.path, key); else if (isSchema(value) && 'deps' in value) (value as any).deps.forEach((path: string) => addNode(path, key)); } return toposort.array(Array.from(nodes), edges).reverse() as string[]; } ================================================ FILE: src/util/toArray.ts ================================================ export default function toArray(value?: null | T | readonly T[]) { return value == null ? [] : ([] as T[]).concat(value); } ================================================ FILE: src/util/types.ts ================================================ export type IfAny = 0 extends 1 & T ? Y : N; export type Maybe = T | null | undefined; export type Preserve = T extends U ? U : never; export type Optionals = Extract; export type Defined = T extends undefined ? never : T; export type NotNull = T extends null ? never : T; /* this seems to force TS to show the full type instead of all the wrapped generics */ export type _ = T extends {} ? { [k in keyof T]: T[k] } : T; // // Schema Config // export type Flags = 's' | 'd' | ''; export type SetFlag = Exclude | F; export type UnsetFlag = Exclude< Old, F > extends never ? '' : Exclude; export type ToggleDefault = Preserve< D, undefined > extends never ? SetFlag : UnsetFlag; export type ResolveFlags = Extract< F, 'd' > extends never ? T : D extends undefined ? T : Defined; export type Concat = NonNullable & NonNullable extends never ? never : (NonNullable & NonNullable) | Optionals; ================================================ FILE: test/.eslintignore ================================================ .eslintrc .eslintrc.js ================================================ FILE: test/ValidationError.ts ================================================ import { describe, it, expect } from 'vitest'; import ValidationError from '../src/ValidationError'; describe('ValidationError', () => { describe('formatError', () => { it('should insert the params into the message', () => { const str = ValidationError.formatError('Some message ${param}', { param: 'here', }); expect(str).toContain('here'); }); it(`should auto include any param named 'label' or 'path' as the 'path' param`, () => { const str = ValidationError.formatError('${path} goes here', { label: 'label', }); expect(str).toContain('label'); }); it(`should use 'this' if a 'label' or 'path' param is not provided`, () => { const str = ValidationError.formatError('${path} goes here', {}); expect(str).toContain('this'); }); it(`should include "undefined" in the message if undefined is provided as a param`, () => { const str = ValidationError.formatError('${path} value is ${min}', { min: undefined, }); expect(str).toContain('undefined'); }); it(`should include "null" in the message if null is provided as a param`, () => { const str = ValidationError.formatError('${path} value is ${min}', { min: null, }); expect(str).toContain('null'); }); it(`should include "NaN" in the message if null is provided as a param`, () => { const str = ValidationError.formatError('${path} value is ${min}', { min: NaN, }); expect(str).toContain('NaN'); }); it(`should include 0 in the message if 0 is provided as a param`, () => { const str = ValidationError.formatError('${path} value is ${min}', { min: 0, }); expect(str).toContain('0'); }); }); it('should disable stacks', () => { const disabled = new ValidationError('error', 1, 'field', 'type', true); expect(disabled.constructor.name).toEqual('ValidationErrorNoStack'); expect(disabled).toBeInstanceOf(ValidationError); }); }); ================================================ FILE: test/array.ts ================================================ import { describe, it, expect, test, vi } from 'vitest'; import { string, number, object, array, StringSchema, AnySchema, ValidationError, } from '../src'; describe('Array types', () => { describe('casting', () => { it('should parse json strings', () => { expect(array().json().cast('[2,3,5,6]')).toEqual([2, 3, 5, 6]); }); it('should failed casts return input', () => { expect(array().cast('asfasf', { assert: false })).toEqual('asfasf'); expect(array().cast('{}', { assert: false })).toEqual('{}'); }); it('should recursively cast fields', () => { expect(array().of(number()).cast(['4', '5'])).toEqual([4, 5]); expect(array().of(string()).cast(['4', 5, false])).toEqual([ '4', '5', 'false', ]); }); it('should pass array options to descendants when casting', async () => { let value = ['1', '2']; let itemSchema = string().when([], function (_, _s, opts: any) { const parent = opts.parent; const idx = opts.index; const val = opts.value; const originalValue = opts.originalValue; expect(parent).toEqual(value); expect(typeof idx).toBe('number'); expect(val).toEqual(parent[idx]); expect(originalValue).toEqual(parent[idx]); return string().transform((value, _originalValue, _schema, options: any) => { expect(parent).toEqual(options.parent); expect(typeof options.index).toBe('number'); expect(val).toEqual(value); return value; }); }); await array().of(itemSchema).validate(value, { context: { name: 'test'} }); }); }); it('should handle DEFAULT', () => { expect(array().getDefault()).toBeUndefined(); expect( array() .default(() => [1, 2, 3]) .getDefault(), ).toEqual([1, 2, 3]); }); it('should type check', () => { let inst = array(); expect(inst.isType([])).toBe(true); expect(inst.isType({})).toBe(false); expect(inst.isType('true')).toBe(false); expect(inst.isType(NaN)).toBe(false); expect(inst.isType(34545)).toBe(false); expect(inst.isType(null)).toBe(false); expect(inst.nullable().isType(null)).toBe(true); }); it('should cast children', () => { expect(array().of(number()).cast(['1', '3'])).toEqual([1, 3]); }); it('should concat subType correctly', async () => { expect(array(number()).concat(array()).innerType).toBeDefined(); let merged = array(number()).concat(array(number().required())); const ve = new ValidationError(''); // expect(ve.toString()).toBe('[object Error]'); expect(Object.prototype.toString.call(ve)).toBe('[object Error]'); expect((merged.innerType as AnySchema).type).toBe('number'); await expect(merged.validateAt('[0]', undefined)).rejects.toThrowError(); }); it('should pass options to children', () => { expect( array(object({ name: string() })).cast([{ id: 1, name: 'john' }], { stripUnknown: true, }), ).toEqual([{ name: 'john' }]); }); describe('validation', () => { test.each([ ['required', undefined, array().required()], ['required', null, array().required()], ['null', null, array()], ['length', [1, 2, 3], array().length(2)], ])('Basic validations fail: %s %p', async (_, value, schema) => { expect(await schema.isValid(value)).toBe(false); }); test.each([ ['required', [], array().required()], ['nullable', null, array().nullable()], ['length', [1, 2, 3], array().length(3)], ])('Basic validations pass: %s %p', async (_, value, schema) => { expect(await schema.isValid(value)).toBe(true); }); it('should allow undefined', async () => { await expect( array().of(number().max(5)).isValid(undefined), ).resolves.toBe(true); }); it('max should replace earlier tests', async () => { expect(await array().max(4).max(10).isValid(Array(5).fill(0))).toBe(true); }); it('min should replace earlier tests', async () => { expect(await array().min(10).min(4).isValid(Array(5).fill(0))).toBe(true); }); it('should respect subtype validations', async () => { let inst = array().of(number().max(5)); await expect(inst.isValid(['gg', 3])).resolves.toBe(false); await expect(inst.isValid([7, 3])).resolves.toBe(false); let value = await inst.validate(['4', 3]); expect(value).toEqual([4, 3]); }); it('should prevent recursive casting', async () => { // @ts-ignore let castSpy = vi.spyOn(StringSchema.prototype, '_cast'); let value = await array(string()).defined().validate([5]); expect(value[0]).toBe('5'); expect(castSpy).toHaveBeenCalledTimes(1); castSpy.mockRestore(); }); }); it('should respect abortEarly', async () => { let inst = array() .of(object({ str: string().required() })) .test('name', 'oops', () => false); await expect(inst.validate([{ str: '' }])).rejects.toEqual( expect.objectContaining({ value: [{ str: '' }], errors: ['oops'], }), ); await expect( inst.validate([{ str: '' }], { abortEarly: false }), ).rejects.toEqual( expect.objectContaining({ value: [{ str: '' }], errors: ['[0].str is a required field', 'oops'], }), ); }); it('should respect disableStackTrace', async () => { let inst = array().of(object({ str: string().required() })); const data = [{ str: undefined }, { str: undefined }]; return Promise.all([ expect(inst.strict().validate(data)).rejects.toHaveProperty('stack'), expect( inst.strict().validate(data, { disableStackTrace: true }), ).rejects.not.toHaveProperty('stack'), ]); }); it('should compact arrays', () => { let arr = ['', 1, 0, 4, false, null], inst = array(); expect(inst.compact().cast(arr)).toEqual([1, 4]); expect(inst.compact((v) => v == null).cast(arr)).toEqual([ '', 1, 0, 4, false, ]); }); it('should ensure arrays', () => { let inst = array().ensure(); const a = [1, 4]; expect(inst.cast(a)).toBe(a); expect(inst.cast(null)).toEqual([]); // nullable is redundant since this should always produce an array // but we want to ensure that null is actually turned into an array expect(inst.nullable().cast(null)).toEqual([]); expect(inst.cast(1)).toEqual([1]); expect(inst.nullable().cast(1)).toEqual([1]); }); it('should pass resolved path to descendants', async () => { let value = ['2', '3']; let expectedPaths = ['[0]', '[1]']; let itemSchema = string().when([], function (_, _s, opts: any) { let path = opts.path; expect(expectedPaths).toContain(path); return string().required(); }); await array().of(itemSchema).validate(value); }); it('should pass deeply resolved path to descendants', async () => { let value = ['2', '3']; let expectedPaths = ['items[0]', 'items[1]']; let itemSchema = string().when([], function (_, _s, opts: any) { let path = opts.path; expect(expectedPaths).toContain(path); return string().required(); }); const schema = object({ items: array().of(itemSchema) }) await schema.validate({ items: value }); }); it('should maintain array sparseness through validation', async () => { let sparseArray = new Array(2); sparseArray[1] = 1; let value = await array().of(number()).validate(sparseArray); expect(0 in sparseArray).toBe(false); expect(0 in value!).toBe(false); // eslint-disable-next-line no-sparse-arrays expect(value).toEqual([, 1]); }); it('should validate empty slots in sparse array', async () => { let sparseArray = new Array(2); sparseArray[1] = 1; await expect( array().of(number().required()).isValid(sparseArray), ).resolves.toEqual(false); }); }); ================================================ FILE: test/bool.ts ================================================ import { describe, it, expect } from 'vitest'; import { bool } from '../src'; import * as TestHelpers from './helpers'; describe('Boolean types', () => { it('should CAST correctly', () => { let inst = bool(); expect(inst.cast('true')).toBe(true); expect(inst.cast('True')).toBe(true); expect(inst.cast('false')).toBe(false); expect(inst.cast('False')).toBe(false); expect(inst.cast(1)).toBe(true); expect(inst.cast(0)).toBe(false); TestHelpers.castAndShouldFail(inst, 'foo'); TestHelpers.castAndShouldFail(inst, 'bar1'); }); it('should handle DEFAULT', () => { let inst = bool(); expect(inst.getDefault()).toBeUndefined(); expect(inst.default(true).required().getDefault()).toBe(true); }); it('should type check', () => { let inst = bool(); expect(inst.isType(1)).toBe(false); expect(inst.isType(false)).toBe(true); expect(inst.isType('true')).toBe(false); expect(inst.isType(NaN)).toBe(false); expect(inst.isType(new Number('foooo'))).toBe(false); expect(inst.isType(34545)).toBe(false); expect(inst.isType(new Boolean(false))).toBe(true); expect(inst.isType(null)).toBe(false); expect(inst.nullable().isType(null)).toBe(true); }); it('bool should VALIDATE correctly', () => { let inst = bool().required(); return Promise.all([ expect(bool().isValid('1')).resolves.toBe(true), expect(bool().strict().isValid(null)).resolves.toBe(false), expect(bool().nullable().isValid(null)).resolves.toBe(true), expect(inst.validate(undefined)).rejects.toEqual( expect.objectContaining({ errors: ['this is a required field'], }), ), ]); }); it('should check isTrue correctly', () => { return Promise.all([ expect(bool().isTrue().isValid(true)).resolves.toBe(true), expect(bool().isTrue().isValid(false)).resolves.toBe(false), ]); }); it('should check isFalse correctly', () => { return Promise.all([ expect(bool().isFalse().isValid(false)).resolves.toBe(true), expect(bool().isFalse().isValid(true)).resolves.toBe(false), ]); }); }); ================================================ FILE: test/date.ts ================================================ import { describe, it, expect } from 'vitest'; import { ref, date } from '../src'; import * as TestHelpers from './helpers'; function isInvalidDate(date: any): date is Date { return date instanceof Date && isNaN(date.getTime()); } describe('Date types', () => { it('should CAST correctly', () => { let inst = date(); expect(inst.cast(new Date())).toBeInstanceOf(Date); expect(inst.cast('jan 15 2014')).toEqual(new Date(2014, 0, 15)); expect(inst.cast('2014-09-23T19:25:25Z')).toEqual(new Date(1411500325000)); // Leading-zero milliseconds expect(inst.cast('2016-08-10T11:32:19.012Z')).toEqual( new Date(1470828739012), ); // Microsecond precision expect(inst.cast('2016-08-10T11:32:19.2125Z')).toEqual( new Date(1470828739212), ); expect(inst.cast(null, { assert: false })).toEqual(null); }); it('should return invalid date for failed non-null casts', function () { let inst = date(); expect(inst.cast(null, { assert: false })).toEqual(null); expect(inst.cast(undefined, { assert: false })).toEqual(undefined); expect(isInvalidDate(inst.cast('', { assert: false }))).toBe(true); expect(isInvalidDate(inst.cast({}, { assert: false }))).toBe(true); }); it('should type check', () => { let inst = date(); expect(inst.isType(new Date())).toBe(true); expect(inst.isType(false)).toBe(false); expect(inst.isType(null)).toBe(false); expect(inst.isType(NaN)).toBe(false); expect(inst.nullable().isType(new Date())).toBe(true); }); it('should VALIDATE correctly', () => { let inst = date().max(new Date(2014, 5, 15)); return Promise.all([ expect(date().isValid(null)).resolves.toBe(false), expect(date().nullable().isValid(null)).resolves.toBe(true), expect(inst.isValid(new Date(2014, 0, 15))).resolves.toBe(true), expect(inst.isValid(new Date(2014, 7, 15))).resolves.toBe(false), expect(inst.isValid('5')).resolves.toBe(true), expect(inst.required().validate(undefined)).rejects.toEqual( expect.objectContaining({ errors: ['this is a required field'], }), ), expect(inst.required().validate(undefined)).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('required'), ), ), expect(inst.validate(null)).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('cannot be null'), ), ), expect(inst.validate({})).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('must be a `date` type'), ), ), ]); }); it('should check MIN correctly', () => { let min = new Date(2014, 3, 15), invalid = new Date(2014, 1, 15), valid = new Date(2014, 5, 15); expect(function () { date().max('hello'); }).toThrowError(TypeError); expect(function () { date().max(ref('$foo')); }).not.toThrowError(); return Promise.all([ expect(date().min(min).isValid(valid)).resolves.toBe(true), expect(date().min(min).isValid(invalid)).resolves.toBe(false), expect(date().min(min).isValid(null)).resolves.toBe(false), expect( date() .min(ref('$foo')) .isValid(valid, { context: { foo: min } }), ).resolves.toBe(true), expect( date() .min(ref('$foo')) .isValid(invalid, { context: { foo: min } }), ).resolves.toBe(false), ]); }); it('should check MAX correctly', () => { let max = new Date(2014, 7, 15), invalid = new Date(2014, 9, 15), valid = new Date(2014, 5, 15); expect(function () { date().max('hello'); }).toThrowError(TypeError); expect(function () { date().max(ref('$foo')); }).not.toThrowError(); return Promise.all([ expect(date().max(max).isValid(valid)).resolves.toBe(true), expect(date().max(max).isValid(invalid)).resolves.toBe(false), expect(date().max(max).nullable().isValid(null)).resolves.toBe(true), expect( date() .max(ref('$foo')) .isValid(valid, { context: { foo: max } }), ).resolves.toBe(true), expect( date() .max(ref('$foo')) .isValid(invalid, { context: { foo: max } }), ).resolves.toBe(false), ]); }); }); ================================================ FILE: test/helpers.ts ================================================ import { describe, it, expect } from 'vitest'; import { ISchema } from '../src/types'; import printValue from '../src/util/printValue'; export let castAndShouldFail = (schema: ISchema, value: any) => { expect(() => schema.cast(value)).toThrowError(TypeError); }; type Options = { invalid?: any[]; valid?: any[]; }; export let castAll = ( inst: ISchema, { invalid = [], valid = [] }: Options, ) => { valid.forEach(([value, result, schema = inst]) => { it(`should cast ${printValue(value)} to ${printValue(result)}`, () => { expect(schema.cast(value)).toBe(result); }); }); invalid.forEach((value) => { it(`should not cast ${printValue(value)}`, () => { castAndShouldFail(inst, value); }); }); }; export let validateAll = ( inst: ISchema, { valid = [], invalid = [] }: Options, ) => { describe('valid:', () => { runValidations(valid, true); }); describe('invalid:', () => { runValidations(invalid, false); }); function runValidations(arr: any[], isValid: boolean) { arr.forEach((config) => { let message = '', value = config, schema = inst; if (Array.isArray(config)) [value, schema, message = ''] = config; it(`${printValue(value)}${message && ` (${message})`}`, async () => { await expect((schema as any).isValid(value)).resolves.toEqual(isValid); }); }); } }; export function validationErrorWithMessages(...errors: any[]) { return expect.objectContaining({ errors, }); } export function ensureSync(fn: () => Promise) { let run = false; let resolve = (t: any) => { if (!run) return t; throw new Error('Did not execute synchronously'); }; let err = (t: any) => { if (!run) throw t; throw new Error('Did not execute synchronously'); }; let result = fn().then(resolve, err); run = true; return result; } ================================================ FILE: test/lazy.ts ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { lazy, object, mixed, ValidationError } from '../src'; describe('lazy', function () { it('should throw on a non-schema value', () => { // @ts-expect-error testing incorrect usage expect(() => lazy(() => undefined).validateSync(undefined)).toThrowError(); }); describe('mapper', () => { const value = 1; let mapper: any; beforeEach(() => { mapper = vi.fn(() => mixed()); }); it('should call with value', () => { lazy(mapper).validate(value); expect(mapper).toHaveBeenCalledWith(value, expect.any(Object)); }); it('should call with context', () => { const context = { a: 1, }; let options = { context }; lazy(mapper).validate(value, options); expect(mapper).toHaveBeenCalledWith(value, options); }); it('should call with context when nested: #1799', () => { let context = { a: 1 }; let value = { lazy: 1 }; let options = { context }; object({ lazy: lazy(mapper), }).validate(value, options); lazy(mapper).validate(value, options); expect(mapper).toHaveBeenCalledWith(value, options); }); it('should allow meta', () => { const meta = { a: 1 }; const schema = lazy(mapper).meta(meta); expect(schema.meta()).toEqual(meta); expect(schema.meta({ added: true })).not.toEqual(schema.meta()); expect(schema.meta({ added: true }).meta()).toEqual({ a: 1, added: true, }); }); it('should allow throwing validation error in builder', async () => { const schema = lazy(() => { throw new ValidationError('oops'); }); await expect(schema.validate(value)).rejects.toThrowError('oops'); await expect(schema.isValid(value)).resolves.toEqual(false); expect(() => schema.validateSync(value)).toThrowError('oops'); const schema2 = lazy(() => { throw new Error('error'); }); // none validation errors are thrown sync to maintain back compat expect(() => schema2.validate(value)).toThrowError('error'); expect(() => schema2.isValid(value)).toThrowError('error'); }); }); }); ================================================ FILE: test/mixed.ts ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { array, bool, lazy, mixed, number, object, reach, ref, Schema, string, tuple, ValidationError, } from '../src'; import ObjectSchema from '../src/object'; import { ISchema } from '../src/types'; import { ensureSync, validateAll } from './helpers'; let noop = () => {}; // @ts-ignore global.YUP_USE_SYNC && it('[internal] normal methods should be running in sync Mode', async () => { let schema = number(); // test negative ensure case await expect(ensureSync(() => Promise.resolve())).rejects.toThrowError( 'Did not execute synchronously', ); // test positive case await expect(ensureSync(() => schema.isValid(1))).resolves.toBe(true); // ensure it fails with the correct message in sync mode await expect( ensureSync(() => schema.validate('john')), ).rejects.toThrowError( /the final value was: `NaN`.+cast from the value `"john"`/, ); }); describe('Mixed Types ', () => { it('is not nullable by default', async () => { let inst = mixed(); await expect(inst.isValid(null)).resolves.toBe(false); await expect(inst.nullable().isValid(null)).resolves.toBe(true); }); it('cast should return a default when undefined', () => { let inst = mixed().default('hello'); expect(inst.cast(undefined)).toBe('hello'); }); it('getDefault should return the default value', function () { let inst = string().default('hi'); expect(inst.getDefault({})).toBe('hi'); expect(inst.getDefault()).toBe('hi'); }); it('getDefault should return the default value using context', function () { let inst = string().when('$foo', { is: 'greet', then: (s) => s.default('hi'), }); expect(inst.getDefault({ context: { foo: 'greet' } })).toBe('hi'); }); it('should use provided check', async () => { let schema = mixed((v): v is string => typeof v === 'string'); // @ts-expect-error narrowed type schema.default(1); expect(schema.isType(1)).toBe(false); expect(schema.isType('foo')).toBe(true); await expect(schema.validate(1)).rejects.toThrowError( /this must match the configured type\. The validated value was: `1`/, ); schema = mixed({ type: 'string', check: (v): v is string => typeof v === 'string', }); // @ts-expect-error narrowed type schema.default(1); expect(schema.isType(1)).toBe(false); expect(schema.isType('foo')).toBe(true); await expect(schema.validate(1)).rejects.toThrowError( /this must be a `string` type/, ); }); it('should allow missing values with the "ignore-optionality" option', () => { expect( string().required().cast(null, { assert: 'ignore-optionality' }), ).toBe(null); expect( string().required().cast(undefined, { assert: 'ignore-optionality' }), ).toBe(undefined); }); it('should warn about null types', async () => { await expect(string().strict().validate(null)).rejects.toThrowError( /this cannot be null/, ); }); it('should validateAt', async () => { const schema = object({ foo: array().of( object({ loose: bool(), bar: string().when('loose', { is: true, otherwise: (s) => s.strict(), }), }), ), }); const value = { foo: [{ bar: 1 }, { bar: 1, loose: true }], }; await expect(schema.validateAt('foo[1].bar', value)).resolves.toBeDefined(); await expect(schema.validateAt('foo[0].bar', value)).rejects.toThrowError( /bar must be a `string` type/, ); }); // xit('should castAt', async () => { // const schema = object({ // foo: array().of( // object({ // loose: bool().default(true), // bar: string(), // }), // ), // }); // const value = { // foo: [{ bar: 1 }, { bar: 1, loose: true }], // }; // schema.castAt('foo[1].bar', value).should.equal('1'); // schema.castAt('foo[0].loose', value).should.equal(true); // }); it('should print the original value', async () => { await expect(number().validate('john')).rejects.toThrowError( /the final value was: `NaN`.+cast from the value `"john"`/, ); }); it('should allow function messages', async () => { await expect( string() .label('My string') .required((d) => `${d.label} is required`) .validate(undefined), ).rejects.toThrowError(/My string is required/); }); it('should check types', async () => { let inst = string().strict().typeError('must be a ${type}!'); await expect(inst.validate(5)).rejects.toEqual( expect.objectContaining({ type: 'typeError', message: 'must be a string!', inner: [], }), ); await expect(inst.validate(5, { abortEarly: false })).rejects.toEqual( expect.objectContaining({ type: undefined, message: 'must be a string!', inner: [expect.any(ValidationError)], }), ); }); it('should limit values', async () => { let inst = mixed().oneOf([5, 'hello']); await expect(inst.isValid(5)).resolves.toBe(true); await expect(inst.isValid('hello')).resolves.toBe(true); await expect(inst.validate(6)).rejects.toThrowError( 'this must be one of the following values: 5, hello', ); }); it('should limit values with a ref', async () => { let someValues = [1, 2, 3]; let context = { someValues }; let inst = mixed().oneOf([ ref('$someValues[0]'), ref('$someValues[1]'), ref('$someValues[2]'), ]); await expect(inst.validate(1, { context })).resolves.toBe(1); await expect(inst.validate(4, { context })).rejects.toEqual( expect.objectContaining({ type: 'oneOf', params: expect.objectContaining({ resolved: someValues }), }), ); }); it('should not require field when notRequired was set', async () => { let inst = mixed().required(); await expect(inst.isValid('test')).resolves.toBe(true); await expect(inst.isValid(1)).resolves.toBe(true); await expect(inst.validate(undefined)).rejects.toThrowError( 'this is a required field', ); const next = inst.notRequired(); await expect(next.isValid(undefined)).resolves.toBe(true); }); // @ts-ignore global.YUP_USE_SYNC && describe('synchronous methods', () => { it('should validate synchronously', async () => { let schema = number(); expect(schema.isValidSync('john')).toBe(false); expect(() => schema.validateSync('john')).toThrowError( /the final value was: `NaN`.+cast from the value `"john"`/, ); }); it('should isValid synchronously', async () => { let schema = number(); expect(schema.isValidSync('john')).toBe(false); }); it('should throw on async test', async () => { let schema = mixed().test('test', 'foo', () => Promise.resolve(true)); await expect( ensureSync(() => schema.validate('john')), ).rejects.toThrowError(/Validation test of type: "test"/); }); }); describe('oneOf', () => { let inst = mixed().oneOf(['hello']); validateAll(inst, { valid: [undefined, 'hello', [null, inst.nullable()]], invalid: [ 'YOLO', [undefined, inst.required(), 'required'], // [null, inst.nullable()], [null, inst.nullable().required(), 'required'], ], }); it('should work with refs', async () => { let inst = object({ foo: string(), bar: string().oneOf([ref('foo'), 'b']), }); await expect( inst.validate({ foo: 'a', bar: 'a' }), ).resolves.toBeDefined(); await expect( inst.validate({ foo: 'foo', bar: 'bar' }), ).rejects.toThrowError(); }); }); describe('should exclude values', () => { let inst = mixed().nullable().notOneOf([5, 'hello']); validateAll(inst, { valid: [6, 'hfhfh', [5, inst.oneOf([5]), '`oneOf` called after'], null], invalid: [5, [null, inst.required(), 'required schema']], }); it('should throw the correct error', async () => { await expect(inst.validate(5)).rejects.toThrowError( 'this must not be one of the following values: 5, hello', ); }); }); it('should run subset of validations first', async () => { let called = false; let inst = string() .strict() .test('test', 'boom', () => (called = true)); await expect(inst.validate(25)).rejects.toThrowError(); expect(called).toBe(false); }); it('should respect strict', () => { let inst = string().equals(['hello', '5']); return Promise.all([ expect(inst.isValid(5)).resolves.toBe(true), expect(inst.strict().isValid(5)).resolves.toBe(false), ]); }); it('should respect abortEarly', () => { let inst = string().trim().min(10); return Promise.all([ expect(inst.strict().validate(' hi ')).rejects.toThrowError( /must be a trimmed string/, ), expect( inst.strict().validate(' hi ', { abortEarly: false }), ).rejects.toThrowError(/2 errors/), ]); }); it('should respect disableStackTrace', () => { // let inst = string().trim(); // return Promise.all([ // expect(inst.strict().validate(' hi ')).rejects.toHaveProperty('stack'), // expect( // inst.strict().validate(' hi ', { disableStackTrace: true }), // ).not.toHaveProperty('stack'), // ]); }); it('should overload test()', () => { let inst = mixed().test('test', noop); expect(inst.tests).toHaveLength(1); expect(inst.tests[0]!.OPTIONS!.test).toBe(noop); expect(inst.tests[0]!.OPTIONS!.message).toBe('${path} is invalid'); }); it('should fallback to default message', async () => { let inst = mixed().test(() => false); await expect(inst.validate('foo')).rejects.toThrowError('this is invalid'); }); it('should allow non string messages', async () => { let message = { key: 'foo' }; let inst = mixed().test('test', message, () => false); expect(inst.tests).toHaveLength(1); expect(inst.tests[0]!.OPTIONS!.message).toBe(message); let err = await inst.validate('foo').catch((err) => err); expect(err.message).toEqual(message); }); it('should dedupe tests with the same test function', () => { let inst = mixed().test('test', ' ', noop).test('test', 'asdasd', noop); expect(inst.tests).toHaveLength(1); expect(inst.tests[0]!.OPTIONS!.message).toBe('asdasd'); }); it('should not dedupe tests with the same test function and different type', () => { let inst = mixed().test('test', ' ', noop).test('test-two', 'asdasd', noop); expect(inst.tests).toHaveLength(2); }); it('should respect exclusive validation', () => { let inst = mixed().test({ message: 'invalid', exclusive: true, name: 'test', test: () => {}, }); //.test({ message: 'also invalid', name: 'test', test: () => {} }); expect(inst.tests).toHaveLength(1); inst = mixed() .test({ message: 'invalid', name: 'test', test: () => {} }) .test({ message: 'also invalid', name: 'test', test: () => {} }); expect(inst.tests).toHaveLength(2); }); it('should non-exclusive tests should stack', () => { let inst = mixed() .test({ name: 'test', message: ' ', test: () => {} }) .test({ name: 'test', message: ' ', test: () => {} }); expect(inst.tests).toHaveLength(2); }); it('should replace existing tests, with exclusive test ', () => { let inst = mixed() .test({ name: 'test', message: ' ', test: noop }) .test({ name: 'test', exclusive: true, message: ' ', test: noop }); expect(inst.tests).toHaveLength(1); }); it('should replace existing exclusive tests, with non-exclusive', () => { let inst = mixed() .test({ name: 'test', exclusive: true, message: ' ', test: () => {} }) .test({ name: 'test', message: ' ', test: () => {} }) .test({ name: 'test', message: ' ', test: () => {} }); expect(inst.tests).toHaveLength(2); }); it('exclusive tests should throw without a name', () => { expect(() => { mixed().test({ message: 'invalid', exclusive: true, test: noop }); }).toThrowError(); }); it('exclusive tests should replace previous ones', async () => { let inst = mixed().test({ message: 'invalid', exclusive: true, name: 'max', test: (v) => v! < 5, }); expect(await inst.isValid(8)).toBe(false); expect( await inst .test({ message: 'invalid', exclusive: true, name: 'max', test: (v) => v! < 10, }) .isValid(8), ).toBe(true); }); it('tests should be called with the correct `this`', async () => { let called = false; let inst = object({ other: mixed(), test: mixed().test({ message: 'invalid', exclusive: true, name: 'max', test() { expect(this.path).toBe('test'); expect(this.parent).toEqual({ other: 5, test: 'hi' }); expect(this.options.context).toEqual({ user: 'jason' }); called = true; return true; }, }), }); await inst.validate( { other: 5, test: 'hi' }, { context: { user: 'jason' } }, ); expect(called).toBe(true); }); it('tests should be able to access nested parent', async () => { let finalFrom: any, finalOptions: any; let testFixture = { firstField: 'test', second: [ { thirdField: 'test3', }, { thirdField: 'test4', }, ], }; let third = object({ thirdField: mixed().test({ test() { finalFrom = this.from!; finalOptions = this.options; return true; }, }), }); let second = array().of(third); let first = object({ firstField: mixed(), second, }); await first.validate(testFixture); expect(finalFrom[0].value).toEqual(testFixture.second[finalOptions.index]); expect(finalFrom[0].schema).toBe(third); expect(finalFrom[1].value).toBe(testFixture); expect(finalFrom[1].schema).toBe(first); }); it('tests can return an error', () => { let inst = mixed().test({ message: 'invalid ${path}', name: 'max', test() { return this.createError({ path: 'my.path' }); }, }); return expect(inst.validate('')).rejects.toEqual( expect.objectContaining({ path: 'my.path', errors: ['invalid my.path'], }), ); }); it('should use returned error path and message', () => { let inst = mixed().test({ message: 'invalid ${path}', name: 'max', test: function () { return this.createError({ message: '${path} nope!', path: 'my.path' }); }, }); return expect(inst.validate({ other: 5, test: 'hi' })).rejects.toEqual( expect.objectContaining({ path: 'my.path', errors: ['my.path nope!'], }), ); }); it('should allow custom validation', async () => { let inst = string().test('name', 'test a', (val) => val === 'jim'); return expect(inst.validate('joe')).rejects.toThrowError('test a'); }); // @ts-ignore !global.YUP_USE_SYNC && it('should fail when the test function returns a rejected Promise', async () => { let inst = string().test(() => { return Promise.reject(new Error('oops an error occurred')); }); return expect(inst.validate('joe')).rejects.toThrowError( 'oops an error occurred', ); }); describe('withMutation', () => { it('should pass the same instance to a provided function', () => { let inst = mixed(); let func = vi.fn(); inst.withMutation(func); expect(func).toHaveBeenCalledWith(inst); }); it('should temporarily make mutable', () => { let inst = mixed(); expect(inst.tests).toHaveLength(0); inst.withMutation((inst) => { inst.test('a', () => true); }); expect(inst.tests).toHaveLength(1); }); it('should return immutability', () => { let inst = mixed(); inst.withMutation(() => {}); expect(inst.tests).toHaveLength(0); inst.test('a', () => true); expect(inst.tests).toHaveLength(0); }); it('should work with nesting', () => { let inst = mixed(); expect(inst.tests).toHaveLength(0); inst.withMutation((inst) => { inst.withMutation((inst) => { inst.test('a', () => true); }); inst.test('b', () => true); }); expect(inst.tests).toHaveLength(2); }); }); describe('concat', () => { let next: ISchema; let inst = object({ str: string().required(), obj: object({ str: string(), }), }); beforeEach(() => { next = inst.concat( object({ str: string().required().trim(), str2: string().required(), obj: object({ str: string().required(), }), }), ); }); it('should have the correct number of tests', () => { expect((reach(next, 'str') as Schema).tests).toHaveLength(2); }); it('should have the tests in the correct order', () => { expect((reach(next, 'str') as Schema).tests[0].OPTIONS?.name).toBe( 'required', ); }); it('should validate correctly', async () => { await expect( inst.isValid({ str: 'hi', str2: 'hi', obj: {} }), ).resolves.toBe(true); await expect( next.validate({ str: ' hi ', str2: 'hi', obj: { str: 'hi' } }), ).resolves.toEqual({ str: 'hi', str2: 'hi', obj: { str: 'hi' }, }); }); it('should throw the correct validation errors', async () => { await expect( next.validate({ str: 'hi', str2: 'hi', obj: {} }), ).rejects.toThrowError('obj.str is a required field'); await expect( next.validate({ str2: 'hi', obj: { str: 'hi' } }), ).rejects.toThrowError('str is a required field'); }); }); it('concat should carry over transforms', async () => { let inst = string().trim(); expect(inst.concat(string().min(4)).cast(' hello ')).toBe('hello'); await expect(inst.concat(string().min(4)).isValid(' he ')).resolves.toBe( false, ); }); it('concat should fail on different types', function () { let inst = string().default('hi'); expect(function () { // @ts-expect-error invalid combo inst.concat(object()); }).toThrowError(TypeError); }); it('concat should not overwrite label and meta with undefined', function () { const testLabel = 'Test Label'; const testMeta = { testField: 'test field', }; let baseSchema = mixed().label(testLabel).meta(testMeta); const otherSchema = mixed(); baseSchema = baseSchema.concat(otherSchema); expect(baseSchema.spec.label).toBe(testLabel); expect(baseSchema.spec.meta?.testField).toBe(testMeta.testField); }); it('concat should allow mixed and other type', function () { let inst = mixed().default('hi'); expect(function () { expect(inst.concat(string()).type).toBe('string'); }).not.toThrowError(TypeError); }); it('concat should validate with mixed and other type', async function () { let inst = mixed().concat(number()); await expect(inst.validate([])).rejects.toThrowError( /must be a `number` type/, ); }); it('concat should maintain undefined defaults', function () { let inst = string().default('hi'); expect( inst.concat(string().default(undefined)).getDefault(), ).toBeUndefined(); }); it('concat should preserve oneOf', async function () { let inst = string().oneOf(['a']).concat(string().default('hi')); await expect(inst.isValid('a')).resolves.toBe(true); }); it('concat should override presence', async function () { let inst = string().required().concat(string().nullable()); await expect(inst.isValid(undefined)).resolves.toBe(true); await expect(inst.isValid(null)).resolves.toBe(true); }); it('gives whitelist precedence to second in concat', async function () { let inst = string() .oneOf(['a', 'b', 'c']) .concat(string().notOneOf(['b'])); await expect(inst.isValid('a')).resolves.toBe(true); await expect(inst.isValid('b')).resolves.toBe(false); await expect(inst.isValid('c')).resolves.toBe(true); }); it('gives blacklist precedence to second in concat', async function () { let inst = string() .notOneOf(['a', 'b', 'c']) .concat(string().oneOf(['b', 'c'])); await expect(inst.isValid('a')).resolves.toBe(false); await expect(inst.isValid('b')).resolves.toBe(true); await expect(inst.isValid('c')).resolves.toBe(true); }); it('concats whitelist with refs', async function () { let inst = object({ x: string().required(), y: string() .oneOf([ref('$x'), 'b', 'c']) .concat(string().notOneOf(['c', ref('$x')])), }); await expect(inst.isValid({ x: 'a', y: 'a' })).resolves.toBe(false); await expect(inst.isValid({ x: 'a', y: 'b' })).resolves.toBe(true); await expect(inst.isValid({ x: 'a', y: 'c' })).resolves.toBe(false); }); it('defaults should be validated but not transformed', function () { let inst = string().trim().default(' hi '); return expect(inst.validate(undefined)).rejects.toThrowError( 'this must be a trimmed string', ); }); it('should handle conditionals', async function () { let inst = mixed().when('prop', { is: 5, then: (s) => s.required('from parent'), }); await expect( inst.validate(undefined, { parent: { prop: 5 } } as any), ).rejects.toThrowError(); await expect( inst.validate(undefined, { parent: { prop: 1 } } as any), ).resolves.toBeUndefined(); await expect( inst.validate('hello', { parent: { prop: 5 } } as any), ).resolves.toBeDefined(); const strInst = string().when('prop', { is: 5, then: (s) => s.required(), otherwise: (s) => s.min(4), }); await expect( strInst.validate(undefined, { parent: { prop: 5 } } as any), ).rejects.toThrowError(); await expect( strInst.validate('hello', { parent: { prop: 1 } } as any), ).resolves.toBeDefined(); await expect( strInst.validate('hel', { parent: { prop: 1 } } as any), ).rejects.toThrowError(); }); it('should handle multiple conditionals', function () { let called = false; let inst = mixed().when(['$prop', '$other'], ([prop, other], schema) => { expect(other).toBe(true); expect(prop).toBe(1); called = true; return schema; }); inst.cast({}, { context: { prop: 1, other: true } }); expect(called).toBe(true); inst = mixed().when(['$prop', '$other'], { is: 5, then: (s) => s.required(), }); return expect( inst.isValid(undefined, { context: { prop: 5, other: 5 } }), ).resolves.toBe(false); }); it('should require context when needed', async function () { let inst = mixed().when('$prop', { is: 5, then: (s) => s.required('from context'), }); await expect( inst.validate(undefined, { context: { prop: 5 } }), ).rejects.toThrowError(); await expect( inst.validate(undefined, { context: { prop: 1 } }), ).resolves.toBeUndefined(); await expect( inst.validate('hello', { context: { prop: 5 } }), ).resolves.toBeDefined(); const strInst = string().when('$prop', { is: (val: any) => val === 5, then: (s) => s.required(), otherwise: (s) => s.min(4), }); await expect( strInst.validate(undefined, { context: { prop: 5 } }), ).rejects.toThrowError(); await expect( strInst.validate('hello', { context: { prop: 1 } }), ).resolves.toBeDefined(); await expect( strInst.validate('hel', { context: { prop: 1 } }), ).rejects.toThrowError(); }); it('should not use context refs in object calculations', function () { let inst = object({ prop: string().when('$prop', { is: 5, then: (s) => s.required('from context'), }), }); expect(inst.getDefault()).toEqual({ prop: undefined }); }); it('should support self references in conditions', async function () { let inst = number().when('.', { is: (value: number) => value > 0, then: (s) => s.min(5), }); await expect(inst.validate(4)).rejects.toThrowError(/must be greater/); await expect(inst.validate(5)).resolves.toBeDefined(); await expect(inst.validate(-1)).resolves.toBeDefined(); }); it('should support conditional single argument as options shortcut', async function () { let inst = number().when({ is: (value: number) => value > 0, then: (s) => s.min(5), }); await expect(inst.validate(4)).rejects.toThrowError(/must be greater/); await expect(inst.validate(5)).resolves.toBeDefined(); await expect(inst.validate(-1)).resolves.toBeDefined(); }); it('should allow nested conditions and lazies', async function () { let inst = string().when('$check', { is: (value: any) => typeof value === 'string', then: (s) => s.when('$check', { is: (value: any) => /hello/.test(value), then: () => lazy(() => string().min(6)), }), }); await expect( inst.validate('pass', { context: { check: false } }), ).resolves.toBeDefined(); await expect( inst.validate('pass', { context: { check: 'hello' } }), ).rejects.toThrowError(/must be at least/); await expect( inst.validate('passes', { context: { check: 'hello' } }), ).resolves.toBeDefined(); }); it('should use label in error message', async function () { let label = 'Label'; let inst = object({ prop: string().required().label(label), }); await expect(inst.validate({})).rejects.toThrowError( `${label} is a required field`, ); }); it('should add meta() data', () => { expect(string().meta({ input: 'foo' }).meta({ foo: 'bar' }).meta()).toEqual( { input: 'foo', foo: 'bar', }, ); }); describe('schema.describe()', () => { let schema: ObjectSchema; beforeEach(() => { schema = object({ lazy: lazy(() => string().nullable()), foo: array(number().integer()).required(), bar: string() .max(2) .default(() => 'a') .meta({ input: 'foo' }) .label('str!') .oneOf(['a', 'b']) .notOneOf([ref('foo')]) .when('lazy', { is: 'entered', then: (s) => s.defined(), }), baz: tuple([string(), number()]), }).when(['dummy'], (_, s) => { return s.shape({ when: string(), }); }); }); it('should describe', () => { expect(schema.describe()).toEqual({ type: 'object', meta: undefined, label: undefined, default: { foo: undefined, bar: 'a', lazy: undefined, baz: undefined, }, nullable: false, optional: true, tests: [], oneOf: [], notOneOf: [], fields: { lazy: { type: 'lazy', meta: undefined, label: undefined, default: undefined, }, foo: { type: 'array', meta: undefined, label: undefined, default: undefined, nullable: false, optional: false, tests: [], oneOf: [], notOneOf: [], innerType: { type: 'number', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [ { name: 'integer', params: undefined, }, ], }, }, bar: { type: 'string', label: 'str!', default: 'a', tests: [{ name: 'max', params: { max: 2 } }], meta: { input: 'foo', }, nullable: false, optional: true, oneOf: ['a', 'b'], notOneOf: [ { type: 'ref', key: 'foo', }, ], }, baz: { type: 'tuple', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, tests: [], oneOf: [], notOneOf: [], innerType: [ { type: 'string', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [], }, { type: 'number', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [], }, ], }, }, }); }); it('should describe with options', () => { expect(schema.describe({ value: { lazy: 'entered' } })).toEqual({ type: 'object', meta: undefined, label: undefined, default: { foo: undefined, bar: 'a', lazy: undefined, baz: undefined, when: undefined, }, nullable: false, optional: true, tests: [], oneOf: [], notOneOf: [], fields: { lazy: { type: 'string', meta: undefined, label: undefined, default: undefined, nullable: true, optional: true, oneOf: [], notOneOf: [], tests: [], }, foo: { type: 'array', meta: undefined, label: undefined, default: undefined, nullable: false, optional: false, tests: [], oneOf: [], notOneOf: [], innerType: { type: 'number', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [ { name: 'integer', params: undefined, }, ], }, }, bar: { type: 'string', label: 'str!', default: 'a', tests: [{ name: 'max', params: { max: 2 } }], meta: { input: 'foo', }, nullable: false, optional: false, oneOf: ['a', 'b'], notOneOf: [ { type: 'ref', key: 'foo', }, ], }, baz: { type: 'tuple', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, tests: [], oneOf: [], notOneOf: [], innerType: [ { type: 'string', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [], }, { type: 'number', meta: undefined, label: undefined, default: undefined, nullable: false, optional: true, oneOf: [], notOneOf: [], tests: [], }, ], }, when: { type: 'string', meta: undefined, label: undefined, default: undefined, notOneOf: [], nullable: false, oneOf: [], optional: true, tests: [], }, }, }); }); }); describe('defined', () => { it('should fail when value is undefined', async () => { let inst = object({ prop: string().defined(), }); await expect(inst.validate({})).rejects.toThrowError( 'prop must be defined', ); }); it('should pass when value is null', async () => { let inst = object({ prop: string().nullable().defined(), }); await expect(inst.isValid({ prop: null })).resolves.toBe(true); }); it('should pass when value is not undefined', async () => { let inst = object({ prop: string().defined(), }); await expect(inst.isValid({ prop: 'prop value' })).resolves.toBe(true); }); }); describe('description options', () => { const schema = object({ name: string(), type: bool(), fancy: string() .label('bad label') .when('type', { is: true, then: (schema) => schema.required().label('good label'), otherwise: (schema) => schema.label('default label'), }), }); it('should pass options', async () => { expect( // @ts-ignore schema.fields.fancy.describe({ parent: { type: true } }).label, ).toBe('good label'); expect( // @ts-ignore schema.fields.fancy.describe({ parent: { type: true } }).optional, ).toBe(false); expect( // @ts-ignore schema.fields.fancy.describe({ parent: { type: false } }).label, ).toEqual('default label'); expect( // @ts-ignore schema.fields.fancy.describe({ parent: { type: false } }).optional, ).toBe(true); }); }); }); ================================================ FILE: test/number.ts ================================================ import { describe, it, expect } from 'vitest'; import * as TestHelpers from './helpers'; import { number, NumberSchema, object, ref } from '../src'; describe('Number types', function () { it('is extensible', () => { class MyNumber extends NumberSchema { foo() { return this; } } new MyNumber().foo().integer().required(); }); describe('casting', () => { let schema = number(); TestHelpers.castAll(schema, { valid: [ ['5', 5], [3, 3], //[new Number(5), 5], [' 5.656 ', 5.656], ], invalid: ['', false, true, new Date(), new Number('foo')], }); it('should round', () => { // @ts-expect-error stricter type than accepted expect(schema.round('ceIl').cast(45.1111)).toBe(46); expect(schema.round().cast(45.444444)).toBe(45); expect(schema.nullable().integer().round().cast(null)).toBeNull(); expect(function () { // @ts-expect-error testing incorrectness schema.round('fasf'); }).toThrowError(TypeError); }); it('should truncate', () => { expect(schema.truncate().cast(45.55)).toBe(45); }); it('should return NaN for failed casts', () => { expect(number().cast('asfasf', { assert: false })).toEqual(NaN); expect(number().cast(new Date(), { assert: false })).toEqual(NaN); expect(number().cast(null, { assert: false })).toEqual(null); }); }); it('should handle DEFAULT', function () { let inst = number().default(0); expect(inst.getDefault()).toBe(0); expect(inst.default(5).required().getDefault()).toBe(5); }); it('should type check', function () { let inst = number(); expect(inst.isType(5)).toBe(true); expect(inst.isType(new Number(5))).toBe(true); expect(inst.isType(new Number('foo'))).toBe(false); expect(inst.isType(false)).toBe(false); expect(inst.isType(null)).toBe(false); expect(inst.isType(NaN)).toBe(false); expect(inst.nullable().isType(null)).toBe(true); }); it('should VALIDATE correctly', function () { let inst = number().min(4); return Promise.all([ expect(number().isValid(null)).resolves.toBe(false), expect(number().nullable().isValid(null)).resolves.toBe(true), expect(number().isValid(' ')).resolves.toBe(false), expect(number().isValid('12abc')).resolves.toBe(false), expect(number().isValid(0xff)).resolves.toBe(true), expect(number().isValid('0xff')).resolves.toBe(true), expect(inst.isValid(5)).resolves.toBe(true), expect(inst.isValid(2)).resolves.toBe(false), expect(inst.required().validate(undefined)).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('required'), ), ), expect(inst.validate(null)).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('cannot be null'), ), ), expect(inst.validate({})).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('must be a `number` type'), ), ), ]); }); describe('min', () => { let schema = number().min(5); TestHelpers.validateAll(schema, { valid: [7, 35738787838, [null, schema.nullable()]], invalid: [2, null, [14, schema.min(10).min(15)]], }); }); describe('max', () => { let schema = number().max(5); TestHelpers.validateAll(schema, { valid: [4, -5222, [null, schema.nullable()]], invalid: [10, null, [16, schema.max(20).max(15)]], }); }); describe('lessThan', () => { let schema = number().lessThan(5); TestHelpers.validateAll(schema, { valid: [4, -10, [null, schema.nullable()]], invalid: [5, 7, null, [14, schema.lessThan(10).lessThan(14)]], }); it('should return default message', async () => { await expect(schema.validate(6)).rejects.toEqual( TestHelpers.validationErrorWithMessages('this must be less than 5'), ); }); }); describe('moreThan', () => { let schema = number().moreThan(5); TestHelpers.validateAll(schema, { valid: [6, 56445435, [null, schema.nullable()]], invalid: [5, -10, null, [64, schema.moreThan(52).moreThan(74)]], }); it('should return default message', async () => { await expect(schema.validate(4)).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('this must be greater than 5'), ), ); }); }); describe('integer', () => { let schema = number().integer(); TestHelpers.validateAll(schema, { valid: [4, -5222, 3.12312e51], invalid: [10.53, 0.1 * 0.2, -34512535.626, new Date()], }); it('should return default message', async () => { await expect(schema.validate(10.53)).rejects.toEqual( TestHelpers.validationErrorWithMessages('this must be an integer'), ); }); }); it('should check POSITIVE correctly', function () { let v = number().positive(); return Promise.all([ expect(v.isValid(7)).resolves.toBe(true), expect(v.isValid(0)).resolves.toBe(false), expect(v.validate(0)).rejects.toEqual( TestHelpers.validationErrorWithMessages( 'this must be a positive number', ), ), ]); }); it('should check NEGATIVE correctly', function () { let v = number().negative(); return Promise.all([ expect(v.isValid(-4)).resolves.toBe(true), expect(v.isValid(0)).resolves.toBe(false), expect(v.validate(10)).rejects.toEqual( TestHelpers.validationErrorWithMessages( 'this must be a negative number', ), ), ]); }); it('should resolve param refs when describing', () => { let schema = number().min(ref('$foo')); expect(schema.describe({ value: 10, context: { foo: 5 } })).toEqual( expect.objectContaining({ tests: [ expect.objectContaining({ params: { min: 5, }, }), ], }), ); let schema2 = object({ x: number().min(0), y: number().min(ref('x')), }).required(); expect( schema2.describe({ value: { x: 10 }, context: { foo: 5 } }).fields.y, ).toEqual( expect.objectContaining({ tests: [ expect.objectContaining({ params: { min: 10, }, }), ], }), ); }); }); ================================================ FILE: test/object.ts ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mixed, string, date, number, bool, array, object, ref, lazy, reach, StringSchema, MixedSchema, } from '../src'; import ObjectSchema from '../src/object'; import { AnyObject } from '../src/types'; import { validationErrorWithMessages } from './helpers'; describe('Object types', () => { describe('casting', () => { let createInst = () => object({ num: number(), str: string(), arr: array().of(number()), dte: date(), nested: object().shape({ str: string() }), arrNested: array().of(object().shape({ num: number() })), stripped: string().strip(), }); let inst: ReturnType; beforeEach(() => { inst = createInst(); }); it('should parse json strings', () => { expect( object({ hello: number() }).json().cast('{ "hello": "5" }'), ).toEqual({ hello: 5, }); }); it('should return input for failed casts', () => { expect(object().cast('dfhdfh', { assert: false })).toBe('dfhdfh'); }); it('should recursively cast fields', () => { let obj = { num: '5', str: 'hello', arr: ['4', 5], dte: '2014-09-23T19:25:25Z', nested: { str: 5 }, arrNested: [{ num: 5 }, { num: '5' }], }; const cast = inst.cast(obj); expect(cast).toEqual({ num: 5, str: 'hello', arr: [4, 5], dte: new Date(1411500325000), nested: { str: '5' }, arrNested: [{ num: 5 }, { num: 5 }], }); expect(cast.arrNested![0]).toBe(obj.arrNested[0]); }); it('should return the same object if all props are already cast', () => { let obj = { num: 5, str: 'hello', arr: [4, 5], dte: new Date(1411500325000), nested: { str: '5' }, arrNested: [{ num: 5 }, { num: 5 }], }; expect(inst.cast(obj)).toBe(obj); }); }); describe('validation', () => { it('should run validations recursively', async () => { let inst = object({ num: number(), str: string(), arr: array().of(number()), dte: date(), nested: object().shape({ str: string().strict() }), arrNested: array().of(object().shape({ num: number() })), stripped: string().strip(), }); let obj: AnyObject = { num: '4', str: 'hello', arr: ['4', 5, 6], dte: '2014-09-23T19:25:25Z', nested: { str: 5 }, arrNested: [{ num: 5 }, { num: '2' }], }; await expect(inst.isValid(undefined)).resolves.toBe(true); await expect(inst.validate(obj)).rejects.toEqual( validationErrorWithMessages(expect.stringContaining('nested.str')), ); obj.nested.str = 'hello'; obj.arrNested[1] = 8; await expect(inst.validate(obj)).rejects.toEqual( validationErrorWithMessages(expect.stringContaining('arrNested[1]')), ); }); it('should prevent recursive casting', async () => { let castSpy = vi.spyOn(StringSchema.prototype, '_cast' as any); let inst = object({ field: string(), }); let value = await inst.validate({ field: 5 }); expect(value.field).toBe('5'); expect(castSpy).toHaveBeenCalledTimes(1); castSpy.mockRestore(); }); it('should respect strict for nested values', async () => { let inst = object({ field: string(), }).strict(); await expect(inst.validate({ field: 5 })).rejects.toThrowError( /must be a `string` type/, ); }); it('should respect strict for nested object values', async () => { let inst = object({ obj: object({ field: string().strict(), }), }); await expect(inst.validate({ obj: { field: 5 } })).rejects.toThrowError( /must be a `string` type/, ); }); it('should respect child schema with strict()', async () => { let inst = object({ field: number().strict(), }); await expect(inst.validate({ field: '5' })).rejects.toThrowError( /must be a `number` type/, ); expect(inst.cast({ field: '5' })).toEqual({ field: 5 }); await expect( object({ port: number().strict().integer(), }).validate({ port: 'asdad' }), ).rejects.toThrowError(); }); it('should handle custom validation', async () => { let inst = object() .shape({ prop: mixed(), other: mixed(), }) .test('test', '${path} oops', () => false); await expect(inst.validate({})).rejects.toThrowError('this oops'); }); it('should not clone during validating', async function () { let inst = object({ num: number(), str: string(), arr: array().of(number()), dte: date(), nested: object().shape({ str: string() }), arrNested: array().of(object().shape({ num: number() })), stripped: string().strip(), }); let base = MixedSchema.prototype.clone; MixedSchema.prototype.clone = function (...args) { // @ts-expect-error private property if (!this._mutate) throw new Error('should not call clone'); return base.apply(this, args); }; try { await inst.validate({ nested: { str: 'jimmm' }, arrNested: [{ num: 5 }, { num: '2' }], }); await inst.validate({ nested: { str: 5 }, arrNested: [{ num: 5 }, { num: '2' }], }); } catch (err) { /* ignore */ } finally { //eslint-disable-line MixedSchema.prototype.clone = base; } }); }); it('should pass options to children', function () { expect( object({ names: object({ first: string(), }), }).cast( { extra: true, names: { first: 'john', extra: true }, }, { stripUnknown: true }, ), ).toEqual({ names: { first: 'john', }, }); }); it('should call shape with constructed with an arg', () => { let inst = object({ prop: mixed(), }); expect(inst.fields.prop).toBeDefined(); }); describe('stripUnknown', () => { it('should remove extra fields', () => { const inst = object({ str: string(), }); expect( inst.cast( { str: 'hi', extra: false, sneaky: undefined }, { stripUnknown: true }, ), ).toStrictEqual({ str: 'hi', }); }); it('should one undefined extra fields', () => { const inst = object({ str: string(), }); expect( inst.cast({ str: 'hi', sneaky: undefined }, { stripUnknown: true }), ).toStrictEqual({ str: 'hi', }); }); }); describe('object defaults', () => { const createSchema = () => object({ nest: object({ str: string().default('hi'), }), }); let objSchema: ReturnType; beforeEach(() => { objSchema = createSchema(); }); it('should expand objects by default', () => { expect(objSchema.getDefault()).toEqual({ nest: { str: 'hi' }, }); }); it('should accept a user provided default', () => { let schema = objSchema.default({ boom: 'hi' }); expect(schema.getDefault()).toEqual({ boom: 'hi', }); }); it('should add empty keys when sub schema has no default', () => { expect( object({ str: string(), nest: object({ str: string() }), }).getDefault(), ).toEqual({ nest: { str: undefined }, str: undefined, }); }); it('should create defaults for missing object fields', () => { expect( object({ prop: mixed(), other: object({ x: object({ b: string() }), }), }).cast({ prop: 'foo' }), ).toEqual({ prop: 'foo', other: { x: { b: undefined } }, }); }); it('should propagate context', () => { const objectWithConditions = object({ child: string().when('$variable', { is: 'foo', then: (s) => s.default('is foo'), otherwise: (s) => s.default('not foo'), }), }); expect( objectWithConditions.getDefault({ context: { variable: 'foo' } }), ).toEqual({ child: 'is foo' }); expect( objectWithConditions.getDefault({ context: { variable: 'somethingElse' }, }), ).toEqual({ child: 'not foo' }); expect(objectWithConditions.getDefault()).toEqual({ child: 'not foo' }); }); it('should respect options when casting to default', () => { const objectWithConditions = object({ child: string().when('$variable', { is: 'foo', then: (s) => s.default('is foo'), otherwise: (s) => s.default('not foo'), }), }); expect( objectWithConditions.cast(undefined, { context: { variable: 'foo' } }), ).toEqual({ child: 'is foo' }); expect( objectWithConditions.cast(undefined, { context: { variable: 'somethingElse' }, }), ).toEqual({ child: 'not foo' }); expect(objectWithConditions.cast(undefined)).toEqual({ child: 'not foo', }); }); }); it('should handle empty keys', () => { let inst = object().shape({ prop: mixed(), }); return Promise.all([ expect(inst.isValid({})).resolves.toBe(true), expect( inst.shape({ prop: mixed().required() }).isValid({}), ).resolves.toBe(false), ]); }); it('should work with noUnknown', () => { let inst = object().shape({ prop: mixed(), other: mixed(), }); return Promise.all([ expect( inst.noUnknown('hi').validate({ extra: 'field' }, { strict: true }), ).rejects.toThrowError('hi'), expect( inst.noUnknown().validate({ extra: 'field' }, { strict: true }), ).rejects.toThrowError(/extra/), ]); }); it('should work with noUnknown override', async () => { let inst = object() .shape({ prop: mixed(), }) .noUnknown() .noUnknown(false); await expect(inst.validate({ extra: 'field' })).resolves.toEqual({ extra: 'field', }); }); it('should work with exact', async () => { let inst = object() .shape({ prop: mixed(), }) .exact(); await expect(inst.validate({ extra: 'field' })).rejects.toThrowError( 'this object contains unknown properties: extra', ); }); it('should strip specific fields', () => { let inst = object().shape({ prop: mixed().strip(false), other: mixed().strip(), }); expect(inst.cast({ other: 'boo', prop: 'bar' })).toEqual({ prop: 'bar', }); }); it('should handle field striping with `when`', () => { let inst = object().shape({ other: bool(), prop: mixed().when('other', { is: true, then: (s) => s.strip(), }), }); expect(inst.cast({ other: true, prop: 'bar' })).toEqual({ other: true, }); }); it('should allow refs', async function () { let schema = object({ quz: ref('baz'), baz: ref('foo.bar'), foo: object({ bar: string(), }), x: ref('$x'), }); let value = await schema.validate( { foo: { bar: 'boom' }, }, { context: { x: 5 } }, ); //console.log(value) expect(value).toEqual({ foo: { bar: 'boom', }, baz: 'boom', quz: 'boom', x: 5, }); }); it('should allow refs with abortEarly false', async () => { let schema = object().shape({ field: string(), dupField: ref('field'), }); await expect( schema.validate( { field: 'test', }, { abortEarly: false }, ), ).resolves.toEqual({ field: 'test', dupField: 'test' }); }); describe('lazy evaluation', () => { let types = { string: string(), number: number(), }; it('should be cast-able', () => { let inst = lazy(() => number()); expect(inst.cast).toBeInstanceOf(Function); expect(inst.cast('4')).toBe(4); }); it('should be validatable', async () => { let inst = lazy(() => string().trim('trim me!').strict()); expect(inst.validate).toBeInstanceOf(Function); try { await inst.validate(' john '); } catch (err: any) { expect(err.message).toBe('trim me!'); } }); it('should resolve to schema', () => { type Nested = { nested: Nested; x: { y: Nested; }; }; let inst: ObjectSchema = object({ nested: lazy(() => inst), x: object({ y: lazy(() => inst), }), }); expect(reach(inst, 'nested').resolve({})).toBe(inst); expect(reach(inst, 'x.y').resolve({})).toBe(inst); }); it('should be passed the value', () => { let passed = false; let inst = object({ nested: lazy((value) => { expect(value).toBe('foo'); passed = true; return string(); }), }); inst.cast({ nested: 'foo' }); expect(passed).toBe(true); }); it('should be passed the options', () => { let opts = {}; let passed = false; let inst = lazy((_, options) => { expect(options).toBe(opts); passed = true; return object(); }); inst.cast({ nested: 'foo' }, opts); expect(passed).toBe(true); }); it('should always return a schema', () => { // @ts-expect-error Incorrect usage expect(() => lazy(() => {}).cast()).toThrowError( /must return a valid schema/, ); }); it('should set the correct path', async () => { type Nested = { str: string | null; nested: Nested; }; let inst: ObjectSchema = object({ str: string().required().nullable(), nested: lazy(() => inst.default(undefined)), }); let value = { nested: { str: null }, str: 'foo', }; try { await inst.validate(value, { strict: true }); } catch (err: any) { expect(err.path).toBe('nested.str'); expect(err.message).toMatch(/required/); } }); it('should set the correct path with dotted keys', async () => { let inst: ObjectSchema = object({ 'dotted.str': string().required().nullable(), nested: lazy(() => inst.default(undefined)), }); let value = { nested: { 'dotted.str': null }, 'dotted.str': 'foo', }; try { await inst.validate(value, { strict: true }); } catch (err: any) { expect(err.path).toBe('nested["dotted.str"]'); expect(err.message).toMatch(/required/); } }); it('should resolve array sub types', async () => { let inst: ObjectSchema = object({ str: string().required().nullable(), nested: array().of(lazy(() => inst.default(undefined))), }); let value = { nested: [{ str: null }], str: 'foo', }; try { await inst.validate(value, { strict: true }); } catch (err: any) { expect(err.path).toBe('nested[0].str'); expect(err.message).toMatch(/required/); } }); it('should resolve for each array item', async () => { let inst = array().of( lazy((value: string | number) => (types as any)[typeof value]), ); let val = await inst.validate(['john', 4], { strict: true }); expect(val).toEqual(['john', 4]); }); }); it('should respect abortEarly', async () => { let inst = object({ nest: object({ str: string().required(), }).test('name', 'oops', () => false), }); return Promise.all([ expect(inst.validate({ nest: { str: '' } })).rejects.toEqual( expect.objectContaining({ value: { nest: { str: '' } }, // path: 'nest', errors: ['oops'], }), ), expect( inst.validate({ nest: { str: '' } }, { abortEarly: false }), ).rejects.toEqual( expect.objectContaining({ value: { nest: { str: '' } }, errors: ['nest.str is a required field', 'oops'], }), ), ]); }); it('should flatten validation errors with abortEarly=false', async () => { let inst = object({ str: string().required(), nest: object({ innerStr: string().required(), num: number().moreThan(5), other: number().test('nested', 'invalid', () => { string().email().min(3).validateSync('f', { abortEarly: false }); return true; }), }).test('name', 'oops', () => false), }); const error = await inst .validate( { str: null, nest: { num: 2, str: undefined } }, { abortEarly: false }, ) .catch((e) => e); expect(error.inner).toMatchInlineSnapshot(` [ [ValidationError: str is a required field], [ValidationError: nest.innerStr is a required field], [ValidationError: nest.num must be greater than 5], [ValidationError: oops], [ValidationError: this must be a valid email], [ValidationError: this must be at least 3 characters], ] `); expect(error.errors).toEqual([ 'str is a required field', 'nest.innerStr is a required field', 'nest.num must be greater than 5', 'oops', 'this must be a valid email', 'this must be at least 3 characters', ]); }); it('should sort errors by insertion order', async () => { let inst = object({ // use `when` to make sure it is validated second foo: string().when('bar', () => string().min(5)), bar: string().required(), }); await expect( inst.validate({ foo: 'foo' }, { abortEarly: false }), ).rejects.toEqual( validationErrorWithMessages( 'foo must be at least 5 characters', 'bar is a required field', ), ); }); it('should respect recursive', () => { let inst = object({ nest: object({ str: string().required(), }), }).test('name', 'oops', () => false); let val = { nest: { str: null } }; return Promise.all([ expect(inst.validate(val, { abortEarly: false })).rejects.toEqual( validationErrorWithMessages(expect.any(String), expect.any(String)), ), expect( inst.validate(val, { abortEarly: false, recursive: false }), ).rejects.toEqual(validationErrorWithMessages('oops')), ]); }); it('partial() should work', async () => { let inst = object({ age: number().required(), name: string().required(), }); await expect(inst.isValid({ age: null, name: '' })).resolves.toEqual(false); await expect(inst.partial().isValid({})).resolves.toEqual(true); await expect(inst.partial().isValid({ age: null })).resolves.toEqual(false); await expect(inst.partial().isValid({ name: '' })).resolves.toEqual(false); }); it('deepPartial() should work', async () => { let inst = object({ age: number().required(), name: string().required(), contacts: array( object({ name: string().required(), age: number().required(), lazy: lazy(() => number().required()), }), ).defined(), }); await expect(inst.isValid({ age: 2, name: 'fs' })).resolves.toEqual(false); await expect( inst.isValid({ age: 2, name: 'fs', contacts: [{}] }), ).resolves.toEqual(false); const instPartial = inst.deepPartial(); await expect( inst.validate({ age: 1, name: 'f', contacts: [{ name: 'f', age: 1 }] }), ).rejects.toThrowError('contacts[0].lazy is a required field'); await expect(instPartial.isValid({})).resolves.toEqual(true); await expect(instPartial.isValid({ contacts: [{}] })).resolves.toEqual( true, ); await expect( instPartial.isValid({ contacts: [{ age: null }] }), ).resolves.toEqual(false); await expect( instPartial.isValid({ contacts: [{ lazy: null }] }), ).resolves.toEqual(false); }); it('should alias or move keys', () => { let inst = object() .shape({ myProp: mixed(), Other: mixed(), }) .from('prop', 'myProp') .from('other', 'Other', true); expect(inst.cast({ prop: 5, other: 6 })).toEqual({ myProp: 5, other: 6, Other: 6, }); }); it('should alias nested keys', () => { let inst = object({ foo: object({ bar: string(), }), // @ts-expect-error FIXME }).from('foo.bar', 'foobar', true); expect(inst.cast({ foo: { bar: 'quz' } })).toEqual({ foobar: 'quz', foo: { bar: 'quz' }, }); }); it('should not move keys when it does not exist', () => { let inst = object() .shape({ myProp: mixed(), }) .from('prop', 'myProp'); expect(inst.cast({ myProp: 5 })).toEqual({ myProp: 5 }); expect(inst.cast({ myProp: 5, prop: 7 })).toEqual({ myProp: 7 }); }); it('should handle conditionals', () => { let inst = object().shape({ noteDate: number() .when('stats.isBig', { is: true, then: (s) => s.min(5), }) .when('other', ([v], schema) => (v === 4 ? schema.max(6) : schema)), stats: object({ isBig: bool() }), other: number() .min(1) .when('stats', { is: 5, then: (s) => s }), }); return Promise.all([ expect( // other makes noteDate too large inst.isValid({ stats: { isBig: true }, rand: 5, noteDate: 7, other: 4, }), ).resolves.toBe(false), expect( inst.isValid({ stats: { isBig: true }, noteDate: 1, other: 4 }), ).resolves.toBe(false), expect( inst.isValid({ stats: { isBig: true }, noteDate: 7, other: 6 }), ).resolves.toBe(true), expect( inst.isValid({ stats: { isBig: true }, noteDate: 7, other: 4 }), ).resolves.toBe(false), expect( inst.isValid({ stats: { isBig: false }, noteDate: 4, other: 4 }), ).resolves.toBe(true), expect( inst.isValid({ stats: { isBig: true }, noteDate: 1, other: 4 }), ).resolves.toBe(false), expect( inst.isValid({ stats: { isBig: true }, noteDate: 6, other: 4 }), ).resolves.toBe(true), ]); }); it('should handle conditionals with unknown dependencies', () => { let inst = object().shape({ value: number().when('isRequired', { is: true, then: (s) => s.required(), }), }); return Promise.all([ expect( inst.isValid({ isRequired: true, value: 1234, }), ).resolves.toBe(true), expect( inst.isValid({ isRequired: true, }), ).resolves.toBe(false), expect( inst.isValid({ isRequired: false, value: 1234, }), ).resolves.toBe(true), expect( inst.isValid({ value: 1234, }), ).resolves.toBe(true), ]); }); it('should handle conditionals synchronously', () => { let inst = object().shape({ knownDependency: bool(), value: number().when(['unknownDependency', 'knownDependency'], { is: true, then: (s) => s.required(), }), }); // expect(() => // inst.validateSync({ // unknownDependency: true, // knownDependency: true, // value: 1234, // }), // ).not.throw(); expect(() => inst.validateSync({ unknownDependency: true, knownDependency: true, }), ).toThrowError(/required/); }); it('should allow opt out of topo sort on specific edges', () => { expect(() => { object().shape({ orgID: number().when('location', ([v], schema) => { return v == null ? schema.required() : schema; }), location: string().when('orgID', (v, schema) => { return v == null ? schema.required() : schema; }), }); }).toThrowError('Cyclic dependency, node was:"location"'); expect(() => { object().shape( { orgID: number().when('location', ([v], schema) => { return v == null ? schema.required() : schema; }), location: string().when('orgID', ([v], schema) => { return v == null ? schema.required() : schema; }), }, [['location', 'orgID']], ); }).not.toThrowError(); }); it('should use correct default when concating', () => { let inst = object({ other: bool(), }).default(undefined); expect(inst.concat(object()).getDefault()).toBeUndefined(); expect(inst.concat(object().default({})).getDefault()).toEqual({}); }); it('should maintain excluded edges when concating', async () => { const schema = object().shape( { a1: string().when('a2', { is: undefined, then: (s) => s.required(), }), a2: string().when('a1', { is: undefined, then: (s) => s.required(), }), }, [['a1', 'a2']], ); await expect( schema.concat(object()).isValid({ a1: null }), ).resolves.toEqual(false); await expect( object().concat(schema).isValid({ a1: null }), ).resolves.toEqual(false); }); it('should handle nested conditionals', () => { let countSchema = number().when('isBig', { is: true, then: (s) => s.min(5), }); let inst = object({ other: bool(), stats: object({ isBig: bool(), count: countSchema, }) .default(undefined) .when('other', { is: true, then: (s) => s.required() }), }); return Promise.all([ expect(inst.validate({ stats: undefined, other: true })).rejects.toEqual( validationErrorWithMessages(expect.stringContaining('required')), ), expect( inst.validate({ stats: { isBig: true, count: 3 }, other: true }), ).rejects.toEqual( validationErrorWithMessages( 'stats.count must be greater than or equal to 5', ), ), expect( inst.validate({ stats: { isBig: true, count: 10 }, other: true }), ).resolves.toEqual({ stats: { isBig: true, count: 10 }, other: true, }), expect( countSchema.validate(10, { context: { isBig: true } }), ).resolves.toEqual(10), ]); }); it('should camelCase keys', () => { let inst = object() .shape({ conStat: number(), caseStatus: number(), hiJohn: number(), }) .camelCase(); expect(inst.cast({ CON_STAT: 5, CaseStatus: 6, 'hi john': 4 })).toEqual({ conStat: 5, caseStatus: 6, hiJohn: 4, }); expect(inst.nullable().cast(null)).toBeNull(); }); it('should CONSTANT_CASE keys', () => { let inst = object() .shape({ CON_STAT: number(), CASE_STATUS: number(), HI_JOHN: number(), }) .constantCase(); expect(inst.cast({ conStat: 5, CaseStatus: 6, 'hi john': 4 })).toEqual({ CON_STAT: 5, CASE_STATUS: 6, HI_JOHN: 4, }); expect(inst.nullable().cast(null)).toBeNull(); }); it('should pick', async () => { let inst = object({ age: number().default(30).required(), name: string().default('pat').required(), color: string().default('red').required(), }); expect(inst.pick(['age', 'name']).getDefault()).toEqual({ age: 30, name: 'pat', }); expect( await inst.pick(['age', 'name']).validate({ age: 24, name: 'Bill' }), ).toEqual({ age: 24, name: 'Bill', }); }); it('should omit', async () => { let inst = object({ age: number().default(30).required(), name: string().default('pat').required(), color: string().default('red').required(), }); expect(inst.omit(['age', 'name']).getDefault()).toEqual({ color: 'red', }); expect( await inst.omit(['age', 'name']).validate({ color: 'mauve' }), ).toEqual({ color: 'mauve' }); }); it('should pick and omit with excluded edges', async () => { const inst = object().shape( { a1: string().when('a2', { is: undefined, then: (schema) => schema.required(), }), a2: string().when('a1', { is: undefined, then: (schema) => schema.required(), }), a3: string().required(), }, [['a1', 'a2']], ); await expect( inst.pick(['a1', 'a2']).isValid({ a1: undefined, a2: 'over9000', }), ).resolves.toEqual(true); await expect( inst.pick(['a1', 'a3']).isValid({ a1: 'required', a3: 'asfasf', }), ).resolves.toEqual(true); await expect( inst.omit(['a1', 'a2']).isValid({ a3: 'asfasf', }), ).resolves.toEqual(true); await expect( inst.omit(['a1']).isValid({ a1: undefined, a3: 'asfasf', }), ).resolves.toEqual(false); }); }); ================================================ FILE: test/setLocale.ts ================================================ import { describe, it, expect } from 'vitest'; import { setLocale } from '../src'; import locale from '../src/locale'; describe('Custom locale', () => { it('should get default locale', () => { expect(locale.string?.email).toBe('${path} must be a valid email'); }); it('should set a new locale', () => { const dict = { string: { email: 'Invalid email', }, }; setLocale(dict); expect(locale.string?.email).toBe(dict.string.email); }); it('should update the main locale', () => { expect(locale.string?.email).toBe('Invalid email'); }); it('should not allow prototype pollution', () => { const payload = JSON.parse( '{"__proto__":{"polluted":"Yes! Its Polluted"}}', ); expect(() => setLocale(payload)).toThrowError(); expect(payload).not.toHaveProperty('polluted'); }); it('should not pollute Object.prototype builtins', () => { const payload: any = { toString: { polluted: 'oh no' } }; expect(() => setLocale(payload)).toThrowError(); expect(Object.prototype.toString).not.toHaveProperty('polluted'); }); }); ================================================ FILE: test/standardSchema.ts ================================================ import { describe, it, expect, test, beforeAll } from 'vitest'; import { string, number, array, bool, object, date, mixed, tuple, lazy, addMethod, } from '../src'; import type { StandardSchemaV1 } from '@standard-schema/spec'; function verifyStandardSchema( schema: StandardSchemaV1, ) { return ( schema['~standard'].version === 1 && schema['~standard'].vendor === 'yup' && typeof schema['~standard'].validate === 'function' ); } /** * Helper function to get Yup validation error directly without try-catch boilerplate */ async function getYupValidationError( schema: any, value: any, options?: any, ): Promise { try { await schema.validate(value, { abortEarly: false, ...options }); return null; // No error occurred } catch (err) { return err; // Return the validation error } } /** * Helper function to compare standard schema validation results with main Yup API */ async function expectValidationConsistency( schema: any, testValue: any, shouldBeValid?: boolean, expectedMessage?: string, options?: any, ) { // Test with standard schema const standardResult = await schema['~standard'].validate(testValue); // Test with main API const mainAPIError = await getYupValidationError(schema, testValue, options); if (shouldBeValid === true) { // Both should succeed expect(standardResult.issues).toBeUndefined(); expect(mainAPIError).toBeNull(); } else if (shouldBeValid === false) { // Both should have errors expect(standardResult.issues).toBeDefined(); expect(mainAPIError).toBeDefined(); if (expectedMessage) { expect(mainAPIError?.message).toEqual( expect.stringContaining(expectedMessage), ); expect(standardResult.issues?.[0]?.message).toEqual( expect.stringContaining(expectedMessage), ); } } else { // Original behavior - just compare that both have errors expect(standardResult.issues).toBeDefined(); expect(mainAPIError).toBeDefined(); // Compare error counts expect(standardResult.issues?.length).toBe(mainAPIError.inner.length); // Compare error messages (sorted for order independence) const standardMessages = standardResult.issues ?.map((issue: any) => issue.message) .sort(); const mainMessages = mainAPIError.inner .map((err: any) => err.message) .sort(); expect(standardMessages).toEqual(mainMessages); } return { standardResult, mainAPIError }; } test('is compatible with standard schema', () => { expect(verifyStandardSchema(string())).toBe(true); expect(verifyStandardSchema(number())).toBe(true); expect(verifyStandardSchema(array())).toBe(true); expect(verifyStandardSchema(bool())).toBe(true); expect(verifyStandardSchema(object())).toBe(true); expect(verifyStandardSchema(date())).toBe(true); expect(verifyStandardSchema(mixed())).toBe(true); expect(verifyStandardSchema(tuple([mixed()]))).toBe(true); expect(verifyStandardSchema(lazy(() => string()))).toBe(true); }); test('issues path is an array of property paths', async () => { const schema = object({ obj: object({ foo: string().required(), 'not.obj.nested': string().required(), }).required(), arr: array( object({ foo: string().required(), 'not.array.nested': string().required(), }), ).required(), 'not.a.field': string().required(), }); const testValue = { obj: { foo: '', 'not.obj.nested': '' }, arr: [{ foo: '', 'not.array.nested': '' }], }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific path structures are correct expect(standardResult.issues?.map((issue: any) => issue.path)).toEqual( expect.arrayContaining([ ['obj', 'foo'], ['obj', 'not.obj.nested'], ['arr', '0', 'foo'], ['arr', '0', 'not.array.nested'], ['not.a.field'], ]), ); }); test('should clone correctly when using modifiers', async () => { const schema = string().required(); const testValue = ''; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify path is undefined for root-level validation expect(standardResult.issues?.[0]?.path).toBeUndefined(); }); test('should work correctly with lazy schemas', async () => { let isNumber = false; const schema = lazy(() => { if (isNumber) { return number().min(10); } return string().required().min(12); }); const testValue1 = ''; // Use helper function to compare validation consistency for string validation const { standardResult: result1 } = await expectValidationConsistency( schema, testValue1, ); // Verify path is undefined for root-level validation expect(result1.issues?.every((issue: any) => issue.path === undefined)).toBe( true, ); isNumber = true; const testValue2 = 5; // Use helper function to compare validation consistency for number validation const { standardResult: result2 } = await expectValidationConsistency( schema, testValue2, ); // Verify path is undefined for root-level validation expect(result2.issues?.[0]?.path).toBeUndefined(); }); describe('Array schema standard interface tests', () => { test('should handle basic array validation', async () => { const schema = array(string()); // Test with valid array const validResult = await schema['~standard'].validate(['a', 'b', 'c']); if (!validResult.issues) { expect(validResult.value).toEqual(['a', 'b', 'c']); } // Test with invalid array items - use an object that can't be cast to string const testValue = ['a', { foo: 'bar' }, 'c']; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details expect(standardResult.issues?.[0]?.path).toEqual(['1']); expect(standardResult.issues?.[0]?.message).toContain( 'must be a `string` type', ); }); test('should handle array length validations', async () => { const schema = array(string()).min(2).max(4); // Test empty array const emptyTestValue: string[] = []; // Use helper function to compare validation consistency for empty array await expectValidationConsistency(schema, emptyTestValue); // Test array too long const longTestValue = ['a', 'b', 'c', 'd', 'e']; // Use helper function to compare validation consistency for long array await expectValidationConsistency(schema, longTestValue); }); test('should handle required array validation', async () => { const schema = array(string()).required(); const testValue = undefined; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify path is undefined for root-level validation expect(standardResult.issues?.[0]?.path).toBeUndefined(); }); test('array validation should produce same errors as main API', async () => { const schema = array(string().required()).min(2); const testValue = ['valid', '']; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); }); describe('Object schema standard interface tests', () => { test('should handle basic object validation', async () => { const schema = object({ name: string().required(), age: number().required().min(0), }); // Test with valid object const validResult = await schema['~standard'].validate({ name: 'John', age: 25, }); if (!validResult.issues) { expect(validResult.value).toEqual({ name: 'John', age: 25 }); } // Test with invalid object const testValue = { name: '', age: -1, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details with flexible matching expect(standardResult.issues).toEqual( expect.arrayContaining([ { path: ['name'], message: expect.stringContaining('required') }, { path: ['age'], message: expect.stringContaining('greater than or equal to 0'), }, ]), ); }); test('should handle nested object validation', async () => { const schema = object({ user: object({ profile: object({ name: string().required(), email: string().email(), }), }), }); const testValue = { user: { profile: { name: '', email: 'invalid-email', }, }, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details expect(standardResult.issues).toEqual( expect.arrayContaining([ { path: ['user', 'profile', 'name'], message: expect.stringContaining('required'), }, { path: ['user', 'profile', 'email'], message: expect.stringContaining('valid email'), }, ]), ); }); test('should handle object with array fields', async () => { const schema = object({ tags: array(string().required()).min(1), metadata: object({ version: number().required(), }).required(), }); const testValue = { tags: [], metadata: null, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details with more flexible matching expect(standardResult.issues).toEqual( expect.arrayContaining([ { path: ['tags'], message: expect.stringContaining('at least 1') }, { path: ['metadata'], message: expect.stringContaining('required') }, ]), ); }); test('object validation should produce same errors as main API', async () => { const schema = object({ email: string().email().required(), age: number().min(18).required(), profile: object({ bio: string().max(100), }), }); const testValue = { email: 'invalid-email', age: 16, profile: { bio: 'x'.repeat(150), }, }; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); }); describe('Lazy schema standard interface tests', () => { test('should handle conditional lazy schema validation', async () => { const schema = lazy((value: any) => { if (typeof value === 'string') { return string().min(5); } if (typeof value === 'number') { return number().max(100); } return mixed().required(); }); // Test string validation const stringTestValue = 'abc'; const { standardResult: stringResult } = await expectValidationConsistency( schema, stringTestValue, ); expect(stringResult.issues?.[0]?.path).toBeUndefined(); // Test number validation const numberTestValue = 150; const { standardResult: numberResult } = await expectValidationConsistency( schema, numberTestValue, ); expect(numberResult.issues?.[0]?.path).toBeUndefined(); // Test successful validation const validStringResult = await schema['~standard'].validate('hello world'); if (!validStringResult.issues) { expect(validStringResult.value).toBe('hello world'); } }); test('should handle lazy schema with object validation', async () => { const schema = lazy((value: any) => { if (value?.type === 'user') { return object({ type: string().oneOf(['user']), name: string().required(), email: string().email().required(), }); } if (value?.type === 'admin') { return object({ type: string().oneOf(['admin']), name: string().required(), permissions: array(string()).min(1), }); } return object({ type: string().required(), }); }); // Test user validation const userTestValue = { type: 'user', name: '', email: 'invalid', }; const { standardResult: userResult } = await expectValidationConsistency( schema, userTestValue, ); expect(userResult.issues).toEqual( expect.arrayContaining([ { path: ['name'], message: expect.stringContaining('required') }, { path: ['email'], message: expect.stringContaining('valid email') }, ]), ); // Test admin validation const adminTestValue = { type: 'admin', name: 'Admin User', permissions: [], }; const { standardResult: adminResult } = await expectValidationConsistency( schema, adminTestValue, ); expect(adminResult.issues).toEqual([ { path: ['permissions'], message: expect.stringContaining('at least 1'), }, ]); }); test('lazy validation should produce same errors as main API', async () => { const schema = lazy((value: any) => { if (Array.isArray(value)) { return array(string().required()).min(2); } return string().required().min(5); }); const testValue = ['valid', '']; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); }); describe('Complex nested validation comparisons', () => { test('should handle deeply nested structure validation identically', async () => { const schema = object({ users: array( object({ id: number().required().positive(), profile: object({ name: string().required().min(2), contact: object({ email: string().email().required(), phone: string().matches(/^\d{10}$/, 'Phone must be 10 digits'), }), }), preferences: array(string()).max(5), }), ).min(1), metadata: object({ version: string().required(), tags: array(string().required()).min(1), }), }); const testValue = { users: [ { id: -1, profile: { name: 'A', contact: { email: 'invalid-email', phone: '123', }, }, preferences: ['a', 'b', 'c', 'd', 'e', 'f'], }, ], metadata: { version: '', tags: [], }, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error paths exist const standardPaths = standardResult.issues?.map((issue: any) => issue.path ? issue.path.join('.') : undefined, ); expect(standardPaths).toContain('users.0.id'); expect(standardPaths).toContain('users.0.profile.name'); expect(standardPaths).toContain('users.0.profile.contact.email'); expect(standardPaths).toContain('users.0.profile.contact.phone'); expect(standardPaths).toContain('users.0.preferences'); expect(standardPaths).toContain('metadata.version'); expect(standardPaths).toContain('metadata.tags'); }); test('should handle successful validation identically', async () => { const schema = object({ name: string().required(), age: number().min(0).max(120), email: string().email(), address: object({ street: string(), city: string().required(), zipCode: string().matches(/^\d{5}$/), }), hobbies: array(string()).max(10), }); const validValue = { name: 'John Doe', age: 30, email: 'john@example.com', address: { street: '123 Main St', city: 'Anytown', zipCode: '12345', }, hobbies: ['reading', 'swimming'], }; // Test with standard schema const standardResult = await schema['~standard'].validate(validValue); // Test with main API const mainResult = await schema.validate(validValue); // Both should succeed expect(standardResult.issues).toBeUndefined(); if (!standardResult.issues) { expect(standardResult.value).toEqual(mainResult); } }); }); describe('Error message consistency tests', () => { test('should produce identical error messages for string validations', async () => { const schema = string().required().min(5).max(10).email(); const testValue = 'abc'; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); test('should produce identical error messages for number validations', async () => { const schema = number().required().min(10).max(100).integer(); const testValue = 5.5; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); test('should handle transform behavior consistently', async () => { const schema = object({ name: string().trim().lowercase(), age: number(), active: bool(), }); const testValue = { name: ' JOHN DOE ', age: '25', active: 'true', }; // Test with standard schema const standardResult = await schema['~standard'].validate(testValue); // Test with main API const mainResult = await schema.validate(testValue); // Both should succeed and produce same transformed values expect(standardResult.issues).toBeUndefined(); if (!standardResult.issues) { expect(standardResult.value).toEqual(mainResult); expect(standardResult.value).toEqual({ name: 'john doe', age: 25, active: true, }); } }); }); describe('Conditional validation tests with when API', () => { test('should handle basic conditional validation', async () => { const schema = object({ isMember: bool(), membershipId: string().when('isMember', { is: true, then: (schema) => schema.required(), otherwise: (schema) => schema.optional(), }), }); // Test when condition is true const testValue = { isMember: true, membershipId: '', }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details with flexible matching expect(standardResult.issues).toEqual([ { path: ['membershipId'], message: expect.stringContaining('required') }, ]); // Test when condition is false const nonMemberResult = await schema['~standard'].validate({ isMember: false, membershipId: '', }); if (!nonMemberResult.issues) { expect(nonMemberResult.value).toEqual({ isMember: false, membershipId: '', }); } }); test('should handle conditional validation with function predicate', async () => { const schema = object({ age: number().required(), parentalConsent: bool().when('age', { is: (age: number) => age < 18, then: (schema) => schema.required(), otherwise: (schema) => schema.optional(), }), }); // Test when condition is true (age < 18) const testValue = { age: 16, parentalConsent: undefined, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details expect(standardResult.issues).toEqual([ { path: ['parentalConsent'], message: expect.stringContaining('required'), }, ]); // Test when condition is false (age >= 18) const adultResult = await schema['~standard'].validate({ age: 25, parentalConsent: undefined, }); if (!adultResult.issues) { expect(adultResult.value).toEqual({ age: 25, parentalConsent: undefined, }); } }); test('should handle multiple conditional dependencies', async () => { const schema = object({ accountType: string().oneOf(['personal', 'business']), hasEmployees: bool(), employeeCount: number().when(['accountType', 'hasEmployees'], { is: (accountType: string, hasEmployees: boolean) => accountType === 'business' && hasEmployees, then: (schema) => schema.required().min(1), otherwise: (schema) => schema.optional(), }), }); // Test when both conditions are met const testValue = { accountType: 'business', hasEmployees: true, employeeCount: undefined, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details expect(standardResult.issues).toEqual([ { path: ['employeeCount'], message: expect.stringContaining('required') }, ]); // Test when conditions are not met const personalAccountResult = await schema['~standard'].validate({ accountType: 'personal', hasEmployees: false, employeeCount: undefined, }); if (!personalAccountResult.issues) { expect(personalAccountResult.value).toEqual({ accountType: 'personal', hasEmployees: false, employeeCount: undefined, }); } }); test('should handle nested conditional validation', async () => { const schema = object({ shippingMethod: string().oneOf([ 'standard', 'express', 'overnight', 'pickup', ]), deliveryAddress: object({ street: string().required(), city: string().required(), state: string().required(), zipCode: string().required(), country: string().required(), }).when('shippingMethod', { is: (method: string) => method !== 'pickup', then: (schema) => schema.required(), otherwise: (schema) => schema.optional(), }), }); // Test with shipping method that requires address but missing address const testValue = { shippingMethod: 'overnight', // deliveryAddress is completely missing }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // When the address is required but missing, we get errors for its required fields expect( standardResult.issues?.some( (issue: any) => issue.path?.join('.') === 'deliveryAddress.street' && issue.message.includes('required'), ), ).toBe(true); // Test with pickup method where address is optional const pickupResult = await schema['~standard'].validate({ shippingMethod: 'pickup', // deliveryAddress is optional for pickup }); if (!pickupResult.issues) { expect(pickupResult.value).toEqual({ shippingMethod: 'pickup', }); } }); test('should handle conditional validation with context variables', async () => { const schema = object({ role: string().oneOf(['user', 'admin']), permissions: array(string()).when('$userRole', { is: 'admin', then: (schema) => schema.min(1).required(), otherwise: (schema) => schema.optional(), }), }); // Test standard schema (doesn't support context, so this should pass) const adminStandardResult = await schema['~standard'].validate({ role: 'user', permissions: [], }); // Note: Standard schema doesn't have context, so this should pass if (!adminStandardResult.issues) { expect(adminStandardResult.value).toEqual({ role: 'user', permissions: [], }); } }); test('conditional validation should produce same errors as main API', async () => { const schema = object({ subscriptionType: string().oneOf(['free', 'premium', 'enterprise']), features: array(string()).when('subscriptionType', { is: 'free', then: (schema) => schema.max(3), otherwise: (schema) => schema.max(20), }), maxUsers: number().when('subscriptionType', { is: 'enterprise', then: (schema) => schema.required().min(50), otherwise: (schema) => schema.optional().max(10), }), }); const testValue = { subscriptionType: 'enterprise', features: Array(25).fill('feature'), // Too many for any plan maxUsers: 5, // Too low for enterprise }; // Use helper function to compare validation consistency await expectValidationConsistency(schema, testValue); }); test('should handle when with function returning schema', async () => { const schema = object({ userType: string().oneOf(['individual', 'organization']), name: string().when('userType', ([userType], schema) => { if (userType === 'organization') { return schema.required().min(2).max(100); } return schema.required().min(1).max(50); }), taxId: string().when('userType', ([userType], schema) => { if (userType === 'organization') { return schema .required() .matches(/^\d{2}-\d{7}$/, 'Tax ID must be in format XX-XXXXXXX'); } return schema.optional(); }), }); // Test organization validation const testValue = { userType: 'organization', name: 'A', // Too short for organization taxId: 'invalid-format', }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific error details with flexible matching expect(standardResult.issues).toEqual( expect.arrayContaining([ { path: ['name'], message: expect.stringContaining('at least 2') }, { path: ['taxId'], message: expect.stringContaining('Tax ID must be in format'), }, ]), ); // Test individual validation const individualResult = await schema['~standard'].validate({ userType: 'individual', name: 'John Doe', taxId: undefined, }); if (!individualResult.issues) { expect(individualResult.value).toEqual({ userType: 'individual', name: 'John Doe', taxId: undefined, }); } }); test('should handle complex conditional chains', async () => { const schema = object({ eventType: string().oneOf(['conference', 'workshop', 'webinar']), isVirtual: bool(), location: string().when(['eventType', 'isVirtual'], { is: (eventType: string, isVirtual: boolean) => eventType !== 'webinar' && !isVirtual, then: (schema) => schema.required().min(5), otherwise: (schema) => schema.optional(), }), platformUrl: string() .url() .when('isVirtual', { is: true, then: (schema) => schema.required(), otherwise: (schema) => schema.optional(), }), capacity: number() .min(1) .when('eventType', { is: 'conference', then: (schema) => schema.min(50).required(), otherwise: (schema) => schema.max(100).optional(), }), }); const testValue = { eventType: 'conference', isVirtual: false, location: '', // Required but empty platformUrl: '', // Not required but invalid URL if provided capacity: 10, // Too low for conference }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( schema, testValue, ); // Verify specific conditional errors const standardPaths = standardResult.issues?.map((issue: any) => issue.path ? issue.path.join('.') : undefined, ); expect(standardPaths).toContain('location'); expect(standardPaths).toContain('capacity'); }); }); describe('Concat API standard interface tests', () => { test('should handle string schema concatenation', async () => { const baseSchema = string().required(); const extendedSchema = string().min(5).max(20); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with invalid value that violates both base and extended rules const testValue = ''; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( concatenatedSchema, testValue, ); // Verify that concatenated schema produces errors for all combined rules expect(standardResult.issues?.length).toBeGreaterThan(0); expect(standardResult.issues?.[0]?.path).toBeUndefined(); // Root level validation }); test('should handle number schema concatenation', async () => { const baseSchema = number().required().min(0); const extendedSchema = number().max(100).integer(); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with invalid value that violates extended rules const testValue = 150.5; // Use helper function to compare validation consistency await expectValidationConsistency(concatenatedSchema, testValue); }); test('should handle object schema concatenation', async () => { const baseSchema = object({ name: string().required(), email: string().email(), }); const extendedSchema = object({ age: number().required().min(18), phone: string().matches(/^\d{10}$/, 'Phone must be 10 digits'), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with value that has errors in both base and extended fields const testValue = { name: '', // Violates base schema email: 'invalid-email', // Violates base schema age: 16, // Violates extended schema phone: '123', // Violates extended schema }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( concatenatedSchema, testValue, ); // Verify that all fields from both schemas are validated const errorPaths = standardResult.issues?.map( (issue: any) => issue.path?.[0], ); expect(errorPaths).toContain('name'); expect(errorPaths).toContain('email'); expect(errorPaths).toContain('age'); expect(errorPaths).toContain('phone'); }); test('should handle array schema concatenation', async () => { const baseSchema = array(string()).required(); const extendedSchema = array().min(2).max(5); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with array that violates extended rules const testValue = ['single-item']; // Less than min length // Use helper function to compare validation consistency await expectValidationConsistency(concatenatedSchema, testValue); }); test('should handle concat with conditional validation', async () => { const baseSchema = object({ type: string().oneOf(['user', 'admin']), name: string().required(), }); const extendedSchema = object({ permissions: array(string()).when('type', { is: 'admin', then: (schema) => schema.required().min(1), otherwise: (schema) => schema.optional(), }), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test admin type without permissions const testValue = { type: 'admin', name: 'Admin User', permissions: [], // Should be required and non-empty for admin }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( concatenatedSchema, testValue, ); // Verify conditional validation works in concatenated schema expect(standardResult.issues).toEqual([ { path: ['permissions'], message: expect.stringContaining('at least 1'), }, ]); }); test('should handle concat with transforms', async () => { const baseSchema = object({ name: string().trim(), email: string().lowercase(), }); const extendedSchema = object({ age: number(), active: bool(), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with transformable values const testValue = { name: ' John Doe ', email: 'JOHN@EXAMPLE.COM', age: '25', active: 'true', }; // Test with standard schema const standardResult = await concatenatedSchema['~standard'].validate( testValue, ); // Test with main API const mainResult = await concatenatedSchema.validate(testValue); // Both should succeed and produce same transformed values expect(standardResult.issues).toBeUndefined(); if (!standardResult.issues) { expect(standardResult.value).toEqual(mainResult); expect(standardResult.value).toEqual({ name: 'John Doe', email: 'john@example.com', age: 25, active: true, }); } }); test('should handle multiple concat operations', async () => { const baseSchema = string().required(); const firstExtension = string().min(3); const secondExtension = string().max(10); const thirdExtension = string().matches( /^[A-Z]/, 'Must start with uppercase letter', ); const concatenatedSchema = baseSchema .concat(firstExtension) .concat(secondExtension) .concat(thirdExtension); // Test with value that violates multiple rules const testValue = 'ab'; // Too short and doesn't start with uppercase // Use helper function to compare validation consistency await expectValidationConsistency(concatenatedSchema, testValue); }); test('should handle concat with nested object schemas', async () => { const baseSchema = object({ user: object({ name: string().required(), email: string().email(), }), }); const extendedSchema = object({ user: object({ age: number().min(18), phone: string().required(), }), metadata: object({ createdAt: date().required(), }), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with nested validation errors const testValue = { user: { name: '', email: 'invalid-email', age: 16, phone: '', }, metadata: { createdAt: null, }, }; // Use helper function to compare validation consistency const { standardResult } = await expectValidationConsistency( concatenatedSchema, testValue, ); // With object concat, the extended schema typically overrides the base schema fields // So we mainly check for the extended schema fields and metadata const errorPaths = standardResult.issues?.map((issue: any) => issue.path ? issue.path.join('.') : undefined, ); expect(errorPaths).toContain('user.age'); expect(errorPaths).toContain('user.phone'); expect(errorPaths).toContain('metadata.createdAt'); }); test('should handle concat with mixed schema types', async () => { const baseSchema = object({ value: mixed().required(), }); const extendedSchema = object({ value: mixed().test('custom', 'Value must be positive', (value: any) => { return typeof value === 'number' ? value > 0 : true; }), description: string().optional(), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with negative number (violates custom test) const testValue = { value: -5, description: 'A negative value', }; // Use helper function to compare validation consistency await expectValidationConsistency(concatenatedSchema, testValue); }); test('should handle successful concat validation', async () => { const baseSchema = object({ name: string().required(), email: string().email().required(), }); const extendedSchema = object({ age: number().min(18).max(120), newsletter: bool().default(false), }); const concatenatedSchema = baseSchema.concat(extendedSchema); // Test with valid data const validValue = { name: 'John Doe', email: 'john@example.com', age: 30, newsletter: true, }; // Test with standard schema const standardResult = await concatenatedSchema['~standard'].validate( validValue, ); // Test with main API const mainResult = await concatenatedSchema.validate(validValue); // Both should succeed expect(standardResult.issues).toBeUndefined(); if (!standardResult.issues) { expect(standardResult.value).toEqual(mainResult); } }); }); describe('AddMethod API standard interface tests', () => { beforeAll(() => { // Add custom methods to string schema addMethod(string, 'isUpperCase', function (message = 'Must be uppercase') { return this.test('isUpperCase', message, function (value: any) { return value == null || value === value.toUpperCase(); }); }); addMethod( string, 'hasMinWords', function (minWords: number, message?: string) { const defaultMessage = `Must have at least ${minWords} words`; return this.test( 'hasMinWords', message || defaultMessage, function (value: any) { if (value == null) return true; return value.trim().split(/\s+/).length >= minWords; }, ); }, ); addMethod( string, 'startsWithCapital', function (message = 'Must start with capital letter') { return this.test('startsWithCapital', message, function (value: any) { return value == null || /^[A-Z]/.test(value); }); }, ); addMethod( string, 'endsWithPeriod', function (message = 'Must end with period') { return this.test('endsWithPeriod', message, function (value: any) { return value == null || value.endsWith('.'); }); }, ); // Add custom methods to number schema addMethod(number, 'isEven', function (message = 'Must be an even number') { return this.test('isEven', message, function (value: any) { return value == null || value % 2 === 0; }); }); addMethod( number, 'isPositiveWhenDefined', function (message = 'Must be positive when defined') { return this.test( 'isPositiveWhenDefined', message, function (value: any) { return value == null || value > 0; }, ); }, ); // Add custom methods to object schema addMethod( object, 'hasRequiredFields', function (fields: string[], message = 'Missing required fields') { return this.test('hasRequiredFields', message, function (value: any) { if (value == null) return true; return fields.every((field) => Object.prototype.hasOwnProperty.call(value, field), ); }); }, ); // Add custom methods to array schema addMethod( array, 'hasNoDuplicates', function (message = 'Array must not contain duplicates') { return this.test('hasNoDuplicates', message, function (value: any) { if (value == null) return true; return new Set(value).size === value.length; }); }, ); // Add custom methods to mixed schema addMethod( mixed, 'isNotEmpty', function (message = 'Value cannot be empty') { return this.test('isNotEmpty', message, function (value: any) { if (value == null) return false; if (typeof value === 'string') return value.trim().length > 0; if (Array.isArray(value)) return value.length > 0; if (typeof value === 'object') return Object.keys(value).length > 0; return true; }); }, ); }); it('should maintain validation consistency with basic string addMethod', async () => { const customStringSchema = (string().nullable() as any).isUpperCase(); const validValues = ['HELLO', 'WORLD', 'TEST', '', undefined, null]; const invalidValues = ['hello', 'Hello', 'HELLO world', 'test']; for (const value of validValues) { await expectValidationConsistency(customStringSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customStringSchema, value, false, 'Must be uppercase', ); } }); it('should maintain validation consistency with number addMethod', async () => { const customNumberSchema = (number().nullable() as any).isEven(); const validValues = [2, 4, 6, 0, -2, undefined, null]; const invalidValues = [1, 3, 5, -1, 7.5]; for (const value of validValues) { await expectValidationConsistency(customNumberSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customNumberSchema, value, false, 'Must be an even number', ); } }); it('should maintain validation consistency with object addMethod', async () => { const customObjectSchema = (object().nullable() as any).hasRequiredFields([ 'name', 'email', ]); const validValues = [ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com', age: 30 }, undefined, null, ]; const invalidValues = [ { name: 'John' }, { email: 'john@example.com' }, { age: 30 }, {}, ]; for (const value of validValues) { await expectValidationConsistency(customObjectSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customObjectSchema, value, false, 'Missing required fields', ); } }); it('should maintain validation consistency with array addMethod', async () => { const customArraySchema = (array().nullable() as any).hasNoDuplicates(); const validValues = [[1, 2, 3], ['a', 'b', 'c'], [], [1], undefined, null]; const invalidValues = [ [1, 2, 2], ['a', 'b', 'a'], [1, 1, 2, 3], ]; for (const value of validValues) { await expectValidationConsistency(customArraySchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customArraySchema, value, false, 'Array must not contain duplicates', ); } }); it('should maintain validation consistency with chained addMethod', async () => { const customStringSchema = (string().nullable() as any) .startsWithCapital() .endsWithPeriod(); const validValues = ['Hello world.', 'Test.', 'A.', undefined, null]; const invalidValues = ['hello world.', 'Test', 'Hello world', 'a.']; for (const value of validValues) { await expectValidationConsistency(customStringSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency(customStringSchema, value, false); } }); it('should maintain validation consistency with parameterized addMethod', async () => { const customStringSchema = (string().nullable() as any).hasMinWords(3); const validValues = [ 'hello world test', 'one two three', 'a b c d', undefined, null, ]; const invalidValues = ['hello', 'hello world', '', ' ']; for (const value of validValues) { await expectValidationConsistency(customStringSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customStringSchema, value, false, 'Must have at least 3 words', ); } }); it('should maintain validation consistency with addMethod on mixed schema', async () => { const customMixedSchema = (mixed().nullable() as any).isNotEmpty(); const validValues = ['hello', 123, [1, 2], { a: 1 }, true]; const invalidValues = ['', ' ', [], {}, null, undefined]; for (const value of validValues) { await expectValidationConsistency(customMixedSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customMixedSchema, value, false, 'Value cannot be empty', ); } }); it('should maintain validation consistency with addMethod and conditional logic', async () => { const customNumberSchema = ( number().nullable() as any ).isPositiveWhenDefined(); // Test without conditional logic first const validValues = [1, 5.5, 100, undefined, null]; const invalidValues = [0, -1, -5.5]; for (const value of validValues) { await expectValidationConsistency(customNumberSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency( customNumberSchema, value, false, 'Must be positive when defined', ); } }); it('should maintain validation consistency with multiple custom methods', async () => { const complexSchema = (string().nullable() as any) .isUpperCase('Must be uppercase') .hasMinWords(2, 'Must have at least 2 words'); const validValues = ['HELLO WORLD', 'TEST CASE', 'A B', undefined, null]; const invalidValues = [ 'hello world', // not uppercase 'HELLO', // not enough words 'hello', // both violations '', // not enough words 'HELLO world', // not uppercase ]; for (const value of validValues) { await expectValidationConsistency(complexSchema, value, true); } for (const value of invalidValues) { await expectValidationConsistency(complexSchema, value, false); } }); it('should maintain validation consistency with addMethod cast behavior', async () => { // Test that custom methods work with casting const customStringSchema = (string().nullable() as any).isUpperCase(); // Test casting behavior expect(customStringSchema.cast(123)).toBe('123'); expect(customStringSchema.cast(true)).toBe('true'); // Test that casting works correctly with validation const castValue = customStringSchema.cast('TEST'); expect(castValue).toBe('TEST'); await expectValidationConsistency(customStringSchema, 'TEST', true); // Test that non-uppercase strings still fail validation even after casting const nonUpperCase = customStringSchema.cast('hello'); expect(nonUpperCase).toBe('hello'); await expectValidationConsistency( customStringSchema, 'hello', false, 'Must be uppercase', ); }); }); ================================================ FILE: test/string.ts ================================================ import { describe, it, expect, assert } from 'vitest'; import * as TestHelpers from './helpers'; import { string, number, object, ref, ValidationError, AnySchema, } from '../src'; describe('String types', () => { describe('casting', () => { let schema = string(); TestHelpers.castAll(schema, { valid: [ [5, '5'], ['3', '3'], // [new String('foo'), 'foo'], ['', ''], [true, 'true'], [false, 'false'], [0, '0'], [null, null, schema.nullable()], [ { toString: () => 'hey', }, 'hey', ], ], invalid: [null, {}, []], }); describe('ensure', () => { let schema = string().ensure(); TestHelpers.castAll(schema, { valid: [ [5, '5'], ['3', '3'], [null, ''], [undefined, ''], [null, '', schema.default('foo')], [undefined, 'foo', schema.default('foo')], ], }); }); it('should trim', () => { expect(schema.trim().cast(' 3 ')).toBe('3'); }); it('should transform to lowercase', () => { expect(schema.lowercase().cast('HellO JohN')).toBe('hello john'); }); it('should transform to uppercase', () => { expect(schema.uppercase().cast('HellO JohN')).toBe('HELLO JOHN'); }); it('should handle nulls', () => { expect( schema.nullable().trim().lowercase().uppercase().cast(null), ).toBeNull(); }); }); it('should handle DEFAULT', function () { let inst = string(); expect(inst.default('my_value').required().getDefault()).toBe('my_value'); }); it('should type check', function () { let inst = string(); expect(inst.isType('5')).toBe(true); expect(inst.isType(new String('5'))).toBe(true); expect(inst.isType(false)).toBe(false); expect(inst.isType(null)).toBe(false); expect(inst.nullable().isType(null)).toBe(true); }); it('should VALIDATE correctly', function () { let inst = string().required().min(4).strict(); return Promise.all([ expect(string().strict().isValid(null)).resolves.toBe(false), expect(string().strict().nullable().isValid(null)).resolves.toBe(true), expect(inst.isValid('hello')).resolves.toBe(true), expect(inst.isValid('hel')).resolves.toBe(false), expect(inst.validate('')).rejects.toEqual( TestHelpers.validationErrorWithMessages(expect.any(String)), ), ]); }); it('should handle NOTREQUIRED correctly', function () { let v = string().required().notRequired(); return Promise.all([ expect(v.isValid(undefined)).resolves.toBe(true), expect(v.isValid('')).resolves.toBe(true), ]); }); it('should check MATCHES correctly', function () { let v = string().matches(/(hi|bye)/, 'A message'); return Promise.all([ expect(v.isValid('hi')).resolves.toBe(true), expect(v.isValid('nope')).resolves.toBe(false), expect(v.isValid('bye')).resolves.toBe(true), ]); }); it('should check MATCHES correctly with global and sticky flags', function () { let v = string().matches(/hi/gy); return Promise.all([ expect(v.isValid('hi')).resolves.toBe(true), expect(v.isValid('hi')).resolves.toBe(true), ]); }); it('MATCHES should include empty strings', () => { let v = string().matches(/(hi|bye)/); return expect(v.isValid('')).resolves.toBe(false); }); it('MATCHES should exclude empty strings', () => { let v = string().matches(/(hi|bye)/, { excludeEmptyString: true }); return expect(v.isValid('')).resolves.toBe(true); }); it('EMAIL should exclude empty strings', () => { let v = string().email(); return expect(v.isValid('')).resolves.toBe(true); }); it('should check MIN correctly', function () { let v = string().min(5); let obj = object({ len: number(), name: string().min(ref('len')), }); return Promise.all([ expect(v.isValid('hiiofff')).resolves.toBe(true), expect(v.isValid('big')).resolves.toBe(false), expect(v.isValid('noffasfasfasf saf')).resolves.toBe(true), expect(v.isValid(null)).resolves.toBe(false), expect(v.nullable().isValid(null)).resolves.toBe(true), expect(obj.isValid({ len: 10, name: 'john' })).resolves.toBe(false), ]); }); it('should check MAX correctly', function () { let v = string().max(5); let obj = object({ len: number(), name: string().max(ref('len')), }); return Promise.all([ expect(v.isValid('adgf')).resolves.toBe(true), expect(v.isValid('bigdfdsfsdf')).resolves.toBe(false), expect(v.isValid('no')).resolves.toBe(true), expect(v.isValid(null)).resolves.toBe(false), expect(v.nullable().isValid(null)).resolves.toBe(true), expect(obj.isValid({ len: 3, name: 'john' })).resolves.toBe(false), ]); }); it('should check LENGTH correctly', function () { let v = string().length(5); let obj = object({ len: number(), name: string().length(ref('len')), }); return Promise.all([ expect(v.isValid('exact')).resolves.toBe(true), expect(v.isValid('sml')).resolves.toBe(false), expect(v.isValid('biiiig')).resolves.toBe(false), expect(v.isValid(null)).resolves.toBe(false), expect(v.nullable().isValid(null)).resolves.toBe(true), expect(obj.isValid({ len: 5, name: 'foo' })).resolves.toBe(false), ]); }); it('should check url correctly', function () { let v = string().url(); return Promise.all([ expect(v.isValid('//www.github.com/')).resolves.toBe(true), expect(v.isValid('https://www.github.com/')).resolves.toBe(true), expect(v.isValid('this is not a url')).resolves.toBe(false), ]); }); it('should check UUID correctly', function () { let v = string().uuid(); return Promise.all([ expect(v.isValid('0c40428c-d88d-4ff0-a5dc-a6755cb4f4d1')).resolves.toBe( true, ), expect(v.isValid('42c4a747-3e3e-42be-af30-469cfb9c1913')).resolves.toBe( true, ), expect(v.isValid('42c4a747-3e3e-zzzz-af30-469cfb9c1913')).resolves.toBe( false, ), expect(v.isValid('this is not a uuid')).resolves.toBe(false), expect(v.isValid('')).resolves.toBe(false), ]); }); describe('DATETIME', function () { it('should check DATETIME correctly', function () { let v = string().datetime(); return Promise.all([ expect(v.isValid('2023-01-09T12:34:56Z')).resolves.toBe(true), expect(v.isValid('1977-00-28T12:34:56.0Z')).resolves.toBe(true), expect(v.isValid('1900-10-29T12:34:56.00Z')).resolves.toBe(true), expect(v.isValid('1000-11-30T12:34:56.000Z')).resolves.toBe(true), expect(v.isValid('4444-12-31T12:34:56.0000Z')).resolves.toBe(true), // Should not allow time zone offset by default expect(v.isValid('2010-04-10T14:06:14+00:00')).resolves.toBe(false), expect(v.isValid('2000-07-11T21:06:14+07:00')).resolves.toBe(false), expect(v.isValid('1999-08-16T07:06:14-07:00')).resolves.toBe(false), expect(v.isValid('this is not a datetime')).resolves.toBe(false), expect(v.isValid('2023-08-16T12:34:56')).resolves.toBe(false), expect(v.isValid('2023-08-1612:34:56Z')).resolves.toBe(false), expect(v.isValid('1970-01-01 00:00:00Z')).resolves.toBe(false), expect(v.isValid('1970-01-01T00:00:00,000Z')).resolves.toBe(false), expect(v.isValid('1970-01-01T0000')).resolves.toBe(false), expect(v.isValid('1970-01-01T00:00.000')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56.Z')).resolves.toBe(false), expect(v.isValid('2023-08-16')).resolves.toBe(false), expect(v.isValid('1970-as-df')).resolves.toBe(false), expect(v.isValid('19700101')).resolves.toBe(false), expect(v.isValid('197001')).resolves.toBe(false), ]); }); it('should support DATETIME allowOffset option', function () { let v = string().datetime({ allowOffset: true }); return Promise.all([ expect(v.isValid('2023-01-09T12:34:56Z')).resolves.toBe(true), expect(v.isValid('2010-04-10T14:06:14+00:00')).resolves.toBe(true), expect(v.isValid('2000-07-11T21:06:14+07:00')).resolves.toBe(true), expect(v.isValid('1999-08-16T07:06:14-07:00')).resolves.toBe(true), expect(v.isValid('1970-01-01T00:00:00+0630')).resolves.toBe(true), ]); }); it('should support DATETIME precision option', function () { let v = string().datetime({ precision: 4 }); return Promise.all([ expect(v.isValid('2023-01-09T12:34:56.0000Z')).resolves.toBe(true), expect(v.isValid('2023-01-09T12:34:56.00000Z')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56.000Z')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56.00Z')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56.0Z')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56.Z')).resolves.toBe(false), expect(v.isValid('2023-01-09T12:34:56Z')).resolves.toBe(false), expect(v.isValid('2010-04-10T14:06:14.0000+00:00')).resolves.toBe( false, ), ]); }); describe('DATETIME error strings', function () { function getErrorString(schema: AnySchema, value: string) { try { schema.validateSync(value); assert.fail('should have thrown validation error'); } catch (e) { const err = e as ValidationError; return err.errors[0]; } } it('should use the default locale string on error', function () { let v = string().datetime(); expect(getErrorString(v, 'asdf')).toBe( 'this must be a valid ISO date-time', ); }); it('should use the allowOffset locale string on error when offset caused error', function () { let v = string().datetime(); expect(getErrorString(v, '2010-04-10T14:06:14+00:00')).toBe( 'this must be a valid ISO date-time with UTC "Z" timezone', ); }); it('should use the precision locale string on error when precision caused error', function () { let v = string().datetime({ precision: 2 }); expect(getErrorString(v, '2023-01-09T12:34:56Z')).toBe( 'this must be a valid ISO date-time with a sub-second precision of exactly 2 digits', ); }); it('should prefer options.message over all default error messages', function () { let msg = 'hello'; let v = string().datetime({ message: msg }); expect(getErrorString(v, 'asdf')).toBe(msg); expect(getErrorString(v, '2010-04-10T14:06:14+00:00')).toBe(msg); v = string().datetime({ message: msg, precision: 2 }); expect(getErrorString(v, '2023-01-09T12:34:56Z')).toBe(msg); }); }); }); // eslint-disable-next-line jest/no-disabled-tests it.skip('should check allowed values at the end', () => { return Promise.all([ expect( string() .required('Required') .notOneOf([ref('$someKey')]) .validate('', { context: { someKey: '' } }), ).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('Ref($someKey)'), ), ), expect( object({ email: string().required('Email Required'), password: string() .required('Password Required') .notOneOf([ref('email')]), }) .validate({ email: '', password: '' }, { abortEarly: false }) .catch(console.log), ).rejects.toEqual( TestHelpers.validationErrorWithMessages( expect.stringContaining('Email Required'), expect.stringContaining('Password Required'), ), ), ]); }); it('should validate transforms', function () { return Promise.all([ expect(string().trim().isValid(' 3 ')).resolves.toBe(true), expect(string().lowercase().isValid('HellO JohN')).resolves.toBe(true), expect(string().uppercase().isValid('HellO JohN')).resolves.toBe(true), expect(string().trim().isValid(' 3 ', { strict: true })).resolves.toBe( false, ), expect( string().lowercase().isValid('HellO JohN', { strict: true }), ).resolves.toBe(false), expect( string().uppercase().isValid('HellO JohN', { strict: true }), ).resolves.toBe(false), ]); }); }); ================================================ FILE: test/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true, "noImplicitAny": true, "types": ["node"], "rootDir": "../" }, "include": ["../src", "."] } ================================================ FILE: test/tuple.ts ================================================ import { describe, it, expect, test } from 'vitest'; import { string, number, object, tuple, mixed } from '../src'; describe('Array types', () => { describe('casting', () => { it('should failed casts return input', () => { expect( tuple([number(), number()]).cast('asfasf', { assert: false }), ).toEqual('asfasf'); }); it('should recursively cast fields', () => { expect(tuple([number(), number()]).cast(['4', '5'])).toEqual([4, 5]); expect( tuple([string(), string(), string()]).cast(['4', 5, false]), ).toEqual(['4', '5', 'false']); }); it('should pass array options to descendants when casting', async () => { let value = ['1', '2']; let itemSchema = string().when([], function (_, _s, opts: any) { const parent = opts.parent; const idx = opts.index; const val = opts.value; const originalValue = opts.originalValue; expect(parent).toEqual(value); expect(typeof idx).toBe('number'); expect(val).toEqual(parent[idx]); expect(originalValue).toEqual(parent[idx]); return string(); }); await tuple([itemSchema, itemSchema]).validate(value); }); }); it('should handle DEFAULT', () => { expect(tuple([number(), number(), number()]).getDefault()).toBeUndefined(); expect( tuple([number(), number(), number()]) .default(() => [1, 2, 3]) .getDefault(), ).toEqual([1, 2, 3]); }); it('should type check', () => { let inst = tuple([number()]); expect(inst.isType([1])).toBe(true); expect(inst.isType({})).toBe(false); expect(inst.isType('true')).toBe(false); expect(inst.isType(NaN)).toBe(false); expect(inst.isType(34545)).toBe(false); expect(inst.isType(null)).toBe(false); expect(inst.nullable().isType(null)).toBe(true); }); it('should pass options to children', () => { expect( tuple([object({ name: string() })]).cast([{ id: 1, name: 'john' }], { stripUnknown: true, }), ).toEqual([{ name: 'john' }]); }); describe('validation', () => { test.each([ ['required', undefined, tuple([mixed()]).required()], ['required', null, tuple([mixed()]).required()], ['null', null, tuple([mixed()])], ])('Basic validations fail: %s %p', async (_, value, schema) => { expect(await schema.isValid(value)).toBe(false); }); test.each([ ['required', ['any'], tuple([mixed()]).required()], ['nullable', null, tuple([mixed()]).nullable()], ])('Basic validations pass: %s %p', async (_, value, schema) => { expect(await schema.isValid(value)).toBe(true); }); it('should allow undefined', async () => { await expect( tuple([number().defined()]).isValid(undefined), ).resolves.toBe(true); }); it('should respect subtype validations', async () => { let inst = tuple([number().max(5), string()]); await expect(inst.isValid(['gg', 'any'])).resolves.toBe(false); await expect(inst.isValid([7, 3])).resolves.toBe(false); expect(await inst.validate(['4', 3])).toEqual([4, '3']); }); it('should use labels', async () => { let schema = tuple([ string().label('name'), number().positive().integer().label('age'), ]); await expect(schema.validate(['James', -24.55])).rejects.toThrow( 'age must be a positive number', ); }); it('should throw useful type error for length', async () => { let schema = tuple([string().label('name'), number().label('age')]); await expect(schema.validate(['James'])).rejects.toThrowError( 'this tuple value has too few items, expected a length of 2 but got 1 for value', ); await expect(schema.validate(['James', 2, 4])).rejects.toThrowError( 'this tuple value has too many items, expected a length of 2 but got 3 for value', ); }); }); }); ================================================ FILE: test/types/.eslintrc.js ================================================ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { tsconfigRootDir: __dirname, project: ['../tsconfig.json'], }, plugins: ['ts-expect'], rules: { 'ts-expect/expect': 'error', }, }; ================================================ FILE: test/types/types.ts ================================================ /* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable no-unused-labels */ import { array, number, string, date, ref, mixed, bool, reach, addMethod, Schema, } from '../../src'; import { create as tuple } from '../../src/tuple'; import { create as lazy } from '../../src/Lazy'; import ObjectSchema, { create as object } from '../../src/object'; import { ResolveFlags, SetFlag, UnsetFlag, _ } from '../../src/util/types'; ResolveFlags: { // $ExpectType string | undefined type _a = ResolveFlags; // $ExpectType string | undefined type _b = ResolveFlags; // $ExpectType string type _c = ResolveFlags; // $ExpectType string | undefined type _d = ResolveFlags; // $ExpectType string type _e = ResolveFlags, string>; // $ExpectType "" type _f = UnsetFlag<'d', 'd'>; // $ExpectType "s" type _f2 = UnsetFlag<'d' | 's', 'd'>; // $ExpectType "" type _f3 = UnsetFlag<'', 'd'>; // $ExpectType "d" type _f4 = SetFlag<'', 'd'>; } Base_methods: { // $ExpectType boolean | undefined bool().oneOf([true, ref('$foo')]).__outputType; // $ExpectType "asf" | "foo" | undefined string().oneOf(['asf', ref<'foo'>('$foo')]).__outputType; // $ExpectType Date | undefined date().oneOf([new Date(), ref('$now')]).__outputType; // $ExpectType number | undefined number().oneOf([1, ref('$foo')]).__outputType; // type s = StringSchema; // type ss = s['__outputType']; } Mixed: { const mxRequired = mixed().required(); // $ExpectType NonNullable mxRequired.cast(undefined); // $ExpectType NonNullable | null mxRequired.nullable().cast(undefined); // $ExpectType NonNullable mxRequired.nullable().nonNullable().cast(undefined); // const mxOptional = mixed().optional(); // $ExpectType string | undefined mxOptional.cast(undefined); // $ExpectType string mxOptional.defined().cast(undefined); // const mxNullableOptional = mixed().nullable().optional(); // $ExpectType string | null | undefined mxNullableOptional.cast(''); // $ExpectType string mxNullableOptional.required().validateSync(''); // const mxNullable = mixed().nullable(); // $ExpectType string | null | undefined mxNullable.validateSync(''); const mxDefined = mixed().default(''); // $ExpectType "" mxDefined.getDefault(); const mxDefault = mixed().nullable().default('').nullable(); // $ExpectType string | null mxDefault.cast(''); // $ExpectType string | null mxDefault.validateSync(''); // $ExpectType MixedSchema const mxDefaultRequired = mixed().nullable().required().default(''); // $ExpectType string mxDefaultRequired.cast(''); // $ExpectType string mxDefaultRequired.validateSync(null); // $ExpectType "foo" | "bar" string<'foo' | 'bar'>().defined().validateSync('foo'); // $ExpectType string | undefined mixed().strip().cast(undefined); // $ExpectType string | undefined mixed().strip().strip(false).cast(undefined); // $ExpectType string | undefined mixed().optional().concat(mixed()).cast(''); // $ExpectType string mixed().optional().concat(mixed().defined()).cast(''); // $ExpectType string | undefined mixed().nullable().concat(mixed()).cast(''); // $ExpectType string | null | undefined mixed() .nullable() .concat(mixed().optional().nullable()) .cast(''); // $ExpectType "foo" | undefined mixed().notRequired().concat(string<'foo'>()).cast(''); // $ExpectType MixedSchema mixed((value): value is string => typeof value === 'string'); // $ExpectType MixedSchema mixed({ type: 'string', check: (value): value is string => typeof value === 'string', }); // $ExpectType string mixed().defined().cast('', { assert: true }); // $ExpectType string | null | undefined mixed().defined().cast('', { assert: 'ignore-optionality' }); // $ExpectType AnyPresentValue | null mixed().defined().nullable().cast(''); } Strings: { const strRequired = string().required(); // $ExpectType string strRequired.cast(undefined); // $ExpectType string | null strRequired.nullable().cast(undefined); // $ExpectType string strRequired.nullable().nonNullable().cast(undefined); // const strOptional = string().optional(); // $ExpectType string | undefined strOptional.cast(undefined); // $ExpectType string strOptional.defined().cast(undefined); // const strNullableOptional = string().nullable().optional(); // $ExpectType string | null | undefined strNullableOptional.cast(''); // $ExpectType string strNullableOptional.required().validateSync(''); // const strNullable = string().nullable(); // $ExpectType string | null | undefined strNullable.validateSync(''); const strDefined = string().default(''); // $ExpectType "" strDefined.getDefault(); const strDefault = string().nullable().default('').nullable().trim(); // $ExpectType string | null strDefault.cast(''); // $ExpectType string | null strDefault.validateSync(''); // $ExpectType StringSchema const strDefaultRequired = string().nullable().required().default('').trim(); // $ExpectType string strDefaultRequired.cast(''); // $ExpectType string strDefaultRequired.validateSync(null); // $ExpectType "foo" | "bar" string<'foo' | 'bar'>().defined().validateSync('foo'); // $ExpectType string | undefined string().strip().cast(undefined); // $ExpectType string | undefined string().strip().strip(false).cast(undefined); // $ExpectType string | undefined string().optional().concat(string()).cast(''); // $ExpectType string string().optional().concat(string().defined()).cast(''); // $ExpectType string | undefined string().nullable().concat(string()).cast(''); // $ExpectType string | null | undefined string().nullable().concat(string().optional().nullable()).cast(''); // $ExpectType "foo" | undefined string().notRequired().concat(string<'foo'>()).cast(''); // $ExpectType "foo" | null string<'foo'>() .notRequired() .concat(string().nullable().default('bar')) .cast(''); // $ExpectType never string<'bar'>().concat(string<'foo'>().defined()).cast(''); // $ExpectType never string<'bar'>().concat(string<'foo'>()).cast(''); // $ExpectType "foo" | "bar" | undefined string().oneOf(['foo', 'bar']).__outputType; // $ExpectType "foo" | "bar" | null | undefined string().nullable().oneOf(['foo', 'bar']).__outputType; } Numbers: { const numRequired = number().required(); // $ExpectType number numRequired.cast(undefined); // $ExpectType number | null numRequired.nullable().cast(undefined); // $ExpectType number numRequired.nullable().nonNullable().cast(undefined); // const numOptional = number().optional(); // $ExpectType number | undefined numOptional.cast(undefined); // $ExpectType number numOptional.defined().cast(undefined); // const numNullableOptional = number().nullable().optional(); // $ExpectType number | null | undefined numNullableOptional.cast(''); // $ExpectType number numNullableOptional.required().validateSync(''); // // const numNullable = number().nullable(); // $ExpectType number | null | undefined numNullable.validateSync(''); const numDefined = number().default(3); // $ExpectType 3 numDefined.getDefault(); const numDefault = number().nullable().default(3).nullable().min(2); // $ExpectType number | null numDefault.cast(''); // $ExpectType number | null numDefault.validateSync(''); // const numDefaultRequired = number().nullable().required().default(3); // $ExpectType number numDefaultRequired.cast(''); // $ExpectType number numDefaultRequired.validateSync(null); // $ExpectType number | undefined number().strip().cast(undefined); // $ExpectType number | undefined number().strip().strip(false).cast(undefined); // $ExpectType 1 | undefined number().notRequired().concat(number<1>()).cast(''); // $ExpectType 1 | null number<1>().notRequired().concat(number().nullable().default(2)).cast(''); // $ExpectType never number<2>().concat(number<1>().defined()).cast(''); // $ExpectType never number<2>().concat(number<1>()).cast(''); } date: { const dtRequired = date().required(); // $ExpectType Date dtRequired.cast(undefined); // $ExpectType Date | null dtRequired.nullable().cast(undefined); // $ExpectType Date dtRequired.nullable().nonNullable().cast(undefined); // const dtOptional = date().optional(); // $ExpectType Date | undefined dtOptional.cast(undefined); // $ExpectType Date dtOptional.defined().cast(undefined); // const dtNullableOptional = date().nullable().optional(); // $ExpectType Date | null | undefined dtNullableOptional.cast(''); // $ExpectType Date dtNullableOptional.required().validateSync(''); // // const dtNullable = date().nullable(); // $ExpectType Date | null | undefined dtNullable.validateSync(''); const dtDefined = date().default(() => new Date()); // $ExpectType Date const _dtDefined = dtDefined.getDefault(); const dtDefault = date() .nullable() .default(() => new Date()) .nullable() .min(new Date()); // $ExpectType Date | null dtDefault.cast(''); // $ExpectType Date | null dtDefault.validateSync(''); // const dtDefaultRequired = date() .nullable() .required() .default(() => new Date()); // $ExpectType Date dtDefaultRequired.cast(''); // $ExpectType Date dtDefaultRequired.validateSync(null); // $ExpectType Date | undefined date().strip().cast(undefined); // $ExpectType Date | undefined date().strip().strip(false).cast(undefined); } bool: { const blRequired = bool().required(); // $ExpectType NonNullable blRequired.cast(undefined); // $ExpectType NonNullable | null blRequired.nullable().cast(undefined); // $ExpectType NonNullable blRequired.nullable().nonNullable().cast(undefined); // const blOptional = bool().optional(); // $ExpectType boolean | undefined blOptional.cast(undefined); // $ExpectType boolean blOptional.defined().cast(undefined); // const blNullableOptional = bool().nullable().optional(); // $ExpectType boolean | null | undefined blNullableOptional.cast(''); // $ExpectType NonNullable blNullableOptional.required().validateSync(''); // // const blNullable = bool().nullable(); // $ExpectType boolean | null | undefined blNullable.validateSync(''); const blDefined = bool().default(false); // $ExpectType false blDefined.getDefault(); // $ExpectType false | undefined bool().isFalse().cast(undefined); // $ExpectType true | undefined bool().isTrue().cast(undefined); const blDefault = bool().nullable().default(true).nullable(); // $ExpectType boolean | null blDefault.cast(''); // $ExpectType boolean | null blDefault.validateSync(''); // const blDefaultRequired = bool().nullable().required().default(true); // $ExpectType NonNullable blDefaultRequired.cast(''); // $ExpectType NonNullable blDefaultRequired.validateSync(null); // $ExpectType boolean | undefined bool().strip().cast(undefined); // $ExpectType boolean | undefined bool().strip().strip(false).cast(undefined); } Lazy: { const l = lazy(() => string().default('asfasf')); // $ExpectType string l.cast(null); const l2 = lazy((v) => v ? string().default('asfasf') : number().required(), ); // $ExpectType string | number l2.cast(null); } Array: { const arrRequired = array().required(); // $ExpectType any[] arrRequired.cast(undefined); // $ExpectType any[] | null arrRequired.nullable().cast(undefined); // $ExpectType any[] arrRequired.nullable().nonNullable().cast(undefined); // const arrOptional = array().optional(); // $ExpectType any[] | undefined arrOptional.cast(undefined); // $ExpectType any[] arrOptional.defined().cast(undefined); // const arrNullableOptional = array().nullable().optional(); // $ExpectType any[] | null | undefined arrNullableOptional.cast(''); // $ExpectType any[] arrNullableOptional.required().validateSync(''); // // const arrNullable = array().nullable(); // $ExpectType any[] | null | undefined arrNullable.validateSync(''); const arrDefined = array().default(() => [] as unknown[]); // $ExpectType unknown[] arrDefined.getDefault(); const arrDefault = array() .optional() .default(() => [] as unknown[]) .nullable() .min(1); // $ExpectType any[] | null arrDefault.cast(''); // $ExpectType any[] | null arrDefault.validateSync(''); // const arrDefaultRequired = array() .nullable() .required() .default(() => [] as unknown[]); // $ExpectType any[] arrDefaultRequired.cast(''); // $ExpectType any[] arrDefaultRequired.validateSync(null); array().default(() => []); // $ExpectType (string | undefined)[] | undefined array(string()).cast(null); // $ExpectType (string | undefined)[] | null array().defined().nullable().of(string()).cast(null); // $ExpectType string[] | undefined array(string().required()).validateSync(null); // $ExpectType string[] array(string().default('')).required().validateSync(null); // $ExpectType string[] | undefined array(string().default('')).validateSync(null); // $ExpectType string[] | null | undefined array(string().default('')).nullable().validateSync(null); // $ExpectType (string | null)[] | undefined array(string().nullable().default('')).validateSync(null); // $ExpectType number[] array() .default([] as number[]) .getDefault(); // $ExpectType (string | null)[] | null array(string().nullable().default('')) .nullable() .default(() => [] as string[]) .validateSync(null); // $ExpectType string[] | undefined array(lazy(() => string().default(''))).validateSync(null); const numList = [1, 2]; // $ExpectType number[] array(number()).default(numList).getDefault(); // $ExpectType (number | undefined)[] array(number()).concat(array(number()).required()).validateSync([]); // $ExpectType any[] | undefined array().strip().cast(undefined); // $ExpectType any[] | undefined array().strip().strip(false).cast(undefined); ArrayConcat: { const arrReq = array(number()).required(); // $ExpectType (number | undefined)[] const _c1 = array(number()).concat(arrReq).validateSync([]); } } Tuple: { // $ExpectType [number, string | undefined, { age: number; }] | undefined tuple([ number().defined(), string(), object({ age: number().required() }), ]).cast([3, 4]); const tplRequired = tuple([ string().required(), string().required(), ]).required(); // $ExpectType [string, string] tplRequired.cast(undefined); // $ExpectType [string, string] | null tplRequired.nullable().cast(undefined); // $ExpectType [string, string] tplRequired.nullable().nonNullable().cast(undefined); // const tplOptional = tuple([ string().required(), string().required(), ]).optional(); // $ExpectType [string, string] | undefined tplOptional.cast(undefined); // $ExpectType [string, string] tplOptional.defined().cast(undefined); // const tplNullableOptional = tuple([string().required(), string().required()]) .nullable() .optional(); // $ExpectType [string, string] | null | undefined tplNullableOptional.cast(''); // $ExpectType [string, string] tplNullableOptional.required().validateSync(''); // const tplNullable = tuple([ string().required(), string().required(), ]).nullable(); // $ExpectType [string, string] | null | undefined tplNullable.validateSync(''); const tplDefined = tuple([string().required(), string().required()]).default( () => ['', ''], ); // $ExpectType [string, string] tplDefined.getDefault(); const tplDefault = tuple([string().required(), string().required()]) .nullable() .default(['', '']) .nullable(); // $ExpectType [string, string] | null tplDefault.cast(''); // $ExpectType [string, string] | null tplDefault.validateSync(''); // $ExpectType TupleSchema<[string, string], AnyObject, [string, string], "d"> const tplDefaultRequired = tuple([string().required(), string().required()]) .nullable() .required() .default(() => ['', '']); // $ExpectType [string, string] tplDefaultRequired.cast(''); // $ExpectType [string, string] tplDefaultRequired.validateSync(null); } Object: { const objRequired = object().required(); // $ExpectType {} objRequired.cast(undefined); // $ExpectType {} | null objRequired.nullable().cast(undefined); // $ExpectType {} objRequired.nullable().nonNullable().cast(undefined); // const objOptional = object().optional(); // FIXME: should not be undefined // $ExpectType {} | undefined objOptional.cast(undefined); // $ExpectType {} objOptional.defined().cast(undefined); // const objNullableOptional = object().nullable().optional(); // FIXME: should not be undefined // $ExpectType {} | null | undefined objNullableOptional.cast(''); // $ExpectType {} objNullableOptional.required().validateSync(''); // // const objNullable = object().nullable(); // $ExpectType {} | null objNullable.validateSync(''); const v = object({ name: string().defined(), colors: array(string().defined()).required(), }).nullable(); // $ExpectType { name: string; colors: string[]; } | null v.cast({}); interface Person { name: string; } const _person: ObjectSchema = object({ name: string().defined() }); const a1 = object({ list: array(number().required()).required(), nested: array( object({ name: string().default(''), }), ), }) .required() .validateSync(undefined); // $ExpectType number[] a1.list; // $ExpectType string | undefined a1.nested?.[0].name; // $ExpectType string a1.nested![0].name; const obj = object({ string: string<'foo'>().defined(), number: number().default(1), removed: number().strip().default(0), ref: ref('string'), nest: object({ other: string(), }), nullObject: object({ other: string(), }).default(null), lazy: lazy(() => number().defined()), }); const cast1 = obj.cast({}); let _f = obj.getDefault(); // $ExpectType string | undefined cast1!.nest!.other; // $ExpectType "foo" cast1!.string; // @ts-expect-error Removed doesn't exist cast1!.removed; // $ExpectType number cast1!.number; // $ExpectType string string().strip().default('').cast(''); // $ExpectType { string?: string | undefined; } const _cast2 = object({ string: string().strip().strip(false), }).cast(undefined); // // Object Defaults // const dflt1 = obj.getDefault(); // $ExpectType 1 dflt1.number; // $ExpectType undefined dflt1.ref; // $ExpectType undefined dflt1.lazy; // $ExpectType undefined dflt1.string; // $ExpectType undefined dflt1.nest.other; // $ExpectType null dflt1.nullObject; const merge = object({ field: string().required(), other: string().default(''), }).shape({ field: number().default(1), name: string(), }); // $ExpectType { name?: string | undefined; other: string; field: number; } merge.cast({}); // $ExpectType number merge.cast({}).field; // $ExpectType string merge.cast({}).other; Concat: { const obj1 = object({ field: string().required(), other: string().default(''), }); const obj2 = object({ field: number().default(1), name: string(), }).nullable(); // $ExpectType { name?: string | undefined; other: string; field: number; } | null obj1.concat(obj2).cast(''); // $ExpectType { name?: string | undefined; other: string; field: number; } obj1.nullable().concat(obj2.nonNullable()).cast(''); // $ExpectType { field: 1; other: ""; name: undefined; } obj1.nullable().concat(obj2.nonNullable()).getDefault(); // $ExpectType null obj1.concat(obj2.default(null)).getDefault(); const optionalNonDefaultedObj = object({ nested: object({ h: number().required(), }) .default(undefined) .optional(), }); // $ExpectType { h: number; } | undefined optionalNonDefaultedObj.cast({}).nested; } SchemaOfDate: { type Employee = { hire_date: Date; name: string; }; const _t: ObjectSchema = object({ name: string().defined(), hire_date: date().defined(), }); } SchemaOfDateArray: { type EmployeeWithPromotions = { promotion_dates: Date[]; name: string; }; const _t: ObjectSchema = object({ name: string().defined(), promotion_dates: array().of(date().defined()).defined(), }); } SchemaOfFileArray: { type DocumentWithFullHistory = { history?: File[]; name: string; }; const _t: ObjectSchema = object({ name: string().defined(), history: array().of(mixed().defined()), }); } ObjectPick: { const schema = object({ age: number(), name: string().required(), }) .nullable() .required(); // $ExpectType number | undefined schema.pick(['age']).validateSync({ age: '1' }).age; // $ExpectType { age?: number | undefined; } schema.pick(['age']).validateSync({ age: '1' }); } ObjectOmit: { const schema = object({ age: number(), name: string().required(), }) .nullable() .required(); // $ExpectType string schema.omit(['age']).validateSync({ name: '1' }).name; // $ExpectType string | undefined schema.omit(['age']).partial().validateSync({ name: '1' }).name; // $ExpectType { name: string; } schema.omit(['age']).validateSync({ name: '1' }); } ObjectPartial: { const schema = object({ // age: number(), name: string().required(), lazy: lazy(() => number().defined()), address: object() .shape({ line1: string().required(), zip: number().required().strip(), }) .default(undefined), }).nullable(); const partial = schema.partial(); // $ExpectType string | undefined partial.validateSync({})!.name; // $ExpectType string partial.validateSync({})!.address!.line1; // $ExpectType number | undefined partial.validateSync({})!.lazy; const deepPartial = schema.deepPartial(); // $ExpectType string | undefined deepPartial.validateSync({})!.name; // $ExpectType string | undefined deepPartial.validateSync({})!.address!.line1; // $ExpectType number | undefined deepPartial.validateSync({})!.lazy; } } // Conditions: { // // $ExpectType NumberSchema | StringSchema // string().when('foo', ([foo], schema) => (foo ? schema.required() : number())); // // $ExpectType StringSchema // string() // .when('foo', ([foo], schema) => (foo ? schema.required() : schema)) // .when('foo', ([foo], schema) => (foo ? schema.required() : schema)); // // $ExpectType NumberSchema | StringSchema // string().when('foo', { // is: true, // then: () => number(), // otherwise: (s) => s.required(), // }); // const result = object({ // foo: bool().defined(), // polyField: mixed().when('foo', { // is: true, // then: () => number(), // otherwise: (s) => s.required(), // }), // }).cast({ foo: true, polyField: '1' }); // // $ExpectType { polyField?: string | number | undefined; foo: boolean; } // result; // mixed() // .when('foo', ([foo]) => (foo ? string() : number())) // .min(1); // } TypeAssigning: { const _schema: ObjectSchema<{ mtime?: Date | null | undefined; toJSON: () => any; }> = object({ mtime: date().nullable(), toJSON: mixed<() => any>().required(), }); } reach: { const obj = object({ string: string<'foo'>().defined(), number: number().default(1), removed: number().strip(), ref: ref<'foo'>('string'), nest: array( object({ other: string(), }), ), nullObject: object({ other: string(), }).default(null), lazy: lazy(() => number().defined()), }); // $ExpectType ISchema | Reference const _1 = reach(obj, 'nest[0].other' as const); // $ExpectType Reference<{ other?: string | undefined; } | undefined> | ISchema<{ other?: string | undefined; } | undefined, AnyObject, any, any> const _2 = reach(obj, 'nest[0]' as const); // $ExpectType Reference<"foo"> | ISchema<"foo", AnyObject, any, any> const _3 = reach(obj, 'ref'); } addMethod: { addMethod(Schema, 'foo', function () { return this.clone(); }); addMethod(string, 'foo', function () { return this.clone(); }); } ================================================ FILE: test/util/parseIsoDate.ts ================================================ /** * This file is a modified version of the test file from the following repository: * Date.parse with progressive enhancement for ISO 8601 * NON-CONFORMANT EDITION. * © 2011 Colin Snover * Released under MIT license. */ import { describe, test, expect } from 'vitest'; import { parseIsoDate } from '../../src/util/parseIsoDate'; const sixHours = 6 * 60 * 60 * 1000; const sixHoursThirty = sixHours + 30 * 60 * 1000; const epochLocalTime = new Date(1970, 0, 1, 0, 0, 0, 0).valueOf(); describe('plain date (no time)', () => { describe('valid dates', () => { test('Unix epoch', () => { const result = parseIsoDate('1970-01-01'); expect(result).toBe(epochLocalTime); }); test('2001', () => { const result = parseIsoDate('2001'); const expected = new Date(2001, 0, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('2001-02', () => { const result = parseIsoDate('2001-02'); const expected = new Date(2001, 1, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('2001-02-03', () => { const result = parseIsoDate('2001-02-03'); const expected = new Date(2001, 1, 3, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-002001', () => { const result = parseIsoDate('-002001'); const expected = new Date(-2001, 0, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-002001-02', () => { const result = parseIsoDate('-002001-02'); const expected = new Date(-2001, 1, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-002001-02-03', () => { const result = parseIsoDate('-002001-02-03'); const expected = new Date(-2001, 1, 3, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('+010000-02', () => { const result = parseIsoDate('+010000-02'); const expected = new Date(10000, 1, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('+010000-02-03', () => { const result = parseIsoDate('+010000-02-03'); const expected = new Date(10000, 1, 3, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-010000-02', () => { const result = parseIsoDate('-010000-02'); const expected = new Date(-10000, 1, 1, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-010000-02-03', () => { const result = parseIsoDate('-010000-02-03'); const expected = new Date(-10000, 1, 3, 0, 0, 0, 0).valueOf(); expect(result).toBe(expected); }); }); describe('invalid dates', () => { test('invalid YYYY (non-digits)', () => { expect(parseIsoDate('asdf')).toBeNaN(); }); test('invalid YYYY-MM-DD (non-digits)', () => { expect(parseIsoDate('1970-as-df')).toBeNaN(); }); test('invalid YYYY-MM- (extra hyphen)', () => { expect(parseIsoDate('1970-01-')).toBe(epochLocalTime); }); test('invalid YYYY-MM-DD (missing hyphens)', () => { expect(parseIsoDate('19700101')).toBe(epochLocalTime); }); test('ambiguous YYYY-MM/YYYYYY (missing plus/minus or hyphen)', () => { expect(parseIsoDate('197001')).toBe(epochLocalTime); }); }); }); describe('date-time', () => { describe('no time zone', () => { test('2001-02-03T04:05', () => { const result = parseIsoDate('2001-02-03T04:05'); const expected = new Date(2001, 1, 3, 4, 5, 0, 0).valueOf(); expect(result).toBe(expected); }); test('2001-02-03T04:05:06', () => { const result = parseIsoDate('2001-02-03T04:05:06'); const expected = new Date(2001, 1, 3, 4, 5, 6, 0).valueOf(); expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007', () => { const result = parseIsoDate('2001-02-03T04:05:06.007'); const expected = new Date(2001, 1, 3, 4, 5, 6, 7).valueOf(); expect(result).toBe(expected); }); }); describe('Z time zone', () => { test('2001-02-03T04:05Z', () => { const result = parseIsoDate('2001-02-03T04:05Z'); const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06Z', () => { const result = parseIsoDate('2001-02-03T04:05:06Z'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007Z', () => { const result = parseIsoDate('2001-02-03T04:05:06.007Z'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7); expect(result).toBe(expected); }); }); describe('offset time zone', () => { test('2001-02-03T04:05-00:00', () => { const result = parseIsoDate('2001-02-03T04:05-00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06-00:00', () => { const result = parseIsoDate('2001-02-03T04:05:06-00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007-00:00', () => { const result = parseIsoDate('2001-02-03T04:05:06.007-00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7); expect(result).toBe(expected); }); test('2001-02-03T04:05+00:00', () => { const result = parseIsoDate('2001-02-03T04:05+00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06+00:00', () => { const result = parseIsoDate('2001-02-03T04:05:06+00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0); expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007+00:00', () => { const result = parseIsoDate('2001-02-03T04:05:06.007+00:00'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7); expect(result).toBe(expected); }); test('2001-02-03T04:05-06:30', () => { const result = parseIsoDate('2001-02-03T04:05-06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0) + sixHoursThirty; expect(result).toBe(expected); }); test('2001-02-03T04:05:06-06:30', () => { const result = parseIsoDate('2001-02-03T04:05:06-06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0) + sixHoursThirty; expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007-06:30', () => { const result = parseIsoDate('2001-02-03T04:05:06.007-06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7) + sixHoursThirty; expect(result).toBe(expected); }); test('2001-02-03T04:05+06:30', () => { const result = parseIsoDate('2001-02-03T04:05+06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0) - sixHoursThirty; expect(result).toBe(expected); }); test('2001-02-03T04:05:06+06:30', () => { const result = parseIsoDate('2001-02-03T04:05:06+06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0) - sixHoursThirty; expect(result).toBe(expected); }); test('2001-02-03T04:05:06.007+06:30', () => { const result = parseIsoDate('2001-02-03T04:05:06.007+06:30'); const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7) - sixHoursThirty; expect(result).toBe(expected); }); }); describe('incomplete dates', () => { test('2001T04:05:06.007', () => { const result = parseIsoDate('2001T04:05:06.007'); const expected = new Date(2001, 0, 1, 4, 5, 6, 7).valueOf(); expect(result).toBe(expected); }); test('2001-02T04:05:06.007', () => { const result = parseIsoDate('2001-02T04:05:06.007'); const expected = new Date(2001, 1, 1, 4, 5, 6, 7).valueOf(); expect(result).toBe(expected); }); test('-010000T04:05', () => { const result = parseIsoDate('-010000T04:05'); const expected = new Date(-10000, 0, 1, 4, 5, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-010000-02T04:05', () => { const result = parseIsoDate('-010000-02T04:05'); const expected = new Date(-10000, 1, 1, 4, 5, 0, 0).valueOf(); expect(result).toBe(expected); }); test('-010000-02-03T04:05', () => { const result = parseIsoDate('-010000-02-03T04:05'); const expected = new Date(-10000, 1, 3, 4, 5, 0, 0).valueOf(); expect(result).toBe(expected); }); }); describe('invalid date-times', () => { test('missing T', () => { expect(parseIsoDate('1970-01-01 00:00:00')).toBe(epochLocalTime); }); test('too many characters in millisecond part', () => { expect(parseIsoDate('1970-01-01T00:00:00.000000')).toBe(epochLocalTime); }); test('comma instead of dot', () => { expect(parseIsoDate('1970-01-01T00:00:00,000')).toBe(epochLocalTime); }); test('missing colon in timezone part', () => { const subject = '1970-01-01T00:00:00+0630'; expect(parseIsoDate(subject)).toBe(Date.parse(subject)); }); test('missing colon in time part', () => { expect(parseIsoDate('1970-01-01T0000')).toBe(epochLocalTime); }); test('msec with missing seconds', () => { expect(parseIsoDate('1970-01-01T00:00.000')).toBeNaN(); }); }); }); ================================================ FILE: test/yup.js ================================================ import { describe, it, expect, test } from 'vitest'; import reach, { getIn } from '../src/util/reach'; import { addMethod, object, array, string, lazy, number, boolean, date, Schema, ObjectSchema, ArraySchema, StringSchema, NumberSchema, BooleanSchema, DateSchema, mixed, MixedSchema, tuple, } from '../src'; describe('Yup', function () { it('cast should not assert on undefined', () => { expect(() => string().cast(undefined)).not.toThrowError(); }); it('cast should assert on undefined cast results', () => { expect(() => string() .defined() .transform(() => undefined) .cast('foo'), ).toThrowError( 'The value of field could not be cast to a value that satisfies the schema type: "string".', ); }); it('cast should respect assert option', () => { expect(() => string().cast(null)).toThrowError(); expect(() => string().cast(null, { assert: false })).not.toThrowError(); }); it('should getIn correctly', async () => { let num = number(); let shape = object({ 'num-1': num }); let inst = object({ num: number().max(4), nested: object({ arr: array().of(shape), }), }); const value = { nested: { arr: [{}, { 'num-1': 2 }] } }; let { schema, parent, parentPath } = getIn( inst, 'nested.arr[1].num-1', value, ); expect(schema).toBe(num); expect(parentPath).toBe('num-1'); expect(parent).toBe(value.nested.arr[1]); }); it('should getIn array correctly', async () => { let num = number(); let shape = object({ 'num-1': num }); let inst = object({ num: number().max(4), nested: object({ arr: array().of(shape), }), }); const value = { nested: { arr: [{}, { 'num-1': 2 }], }, }; const { schema, parent, parentPath } = getIn(inst, 'nested.arr[1]', value); expect(schema).toBe(shape); expect(parentPath).toBe('1'); expect(parent).toBe(value.nested.arr); }); it('should REACH correctly', async () => { let num = number(); let shape = object({ num }); let inst = object({ num: number().max(4), nested: tuple([ string(), object({ arr: array().of(shape), }), ]), }); expect(reach(inst, '')).toBe(inst); expect(reach(inst, 'nested[1].arr[0].num')).toBe(num); expect(reach(inst, 'nested[1].arr[].num')).toBe(num); expect(reach(inst, 'nested[1].arr.num')).toBe(num); expect(reach(inst, 'nested[1].arr[1].num')).toBe(num); expect(reach(inst, 'nested[1].arr[1]')).toBe(shape); expect(() => reach(inst, 'nested.arr[1].num')).toThrowError( 'Yup.reach cannot implicitly index into a tuple type. the path part ".nested" must contain an index to the tuple element, e.g. ".nested[0]"', ); await expect(reach(inst, 'nested[1].arr[0].num').isValid(5)).resolves.toBe( true, ); }); it('should REACH conditionally correctly', async function () { let num = number().oneOf([4]), inst = object().shape({ num: number().max(4), nested: object().shape({ arr: array().when('$bar', function ([bar]) { return bar !== 3 ? array().of(number()) : array().of( object().shape({ foo: number(), num: number().when('foo', ([foo]) => { if (foo === 5) return num; }), }), ); }), }), }); let context = { bar: 3 }; let value = { bar: 3, nested: { arr: [{ foo: 5 }, { foo: 3 }], }, }; let options = {}; options.parent = value.nested.arr[0]; options.value = options.parent.num; expect(reach(inst, 'nested.arr.num', value).resolve(options)).toBe(num); expect(reach(inst, 'nested.arr[].num', value).resolve(options)).toBe(num); options.context = context; expect(reach(inst, 'nested.arr.num', value, context).resolve(options)).toBe( num, ); expect( reach(inst, 'nested.arr[].num', value, context).resolve(options), ).toBe(num); expect( reach(inst, 'nested.arr[0].num', value, context).resolve(options), ).toBe(num); // // should fail b/c item[1] is used to resolve the schema options.parent = value.nested.arr[1]; options.value = options.parent.num; expect( reach(inst, 'nested["arr"][1].num', value, context).resolve(options), ).not.toBe(num); let reached = reach(inst, 'nested.arr[].num', value, context); await expect( reached.validate(5, { context, parent: { foo: 4 } }), ).resolves.toBeDefined(); await expect( reached.validate(5, { context, parent: { foo: 5 } }), ).rejects.toThrowError(/one of the following/); }); it('should reach through lazy', async () => { let types = { 1: object({ foo: string() }), 2: object({ foo: number() }), }; await expect( object({ x: array(lazy((val) => types[val.type])), }) .strict() .validate({ x: [ { type: 1, foo: '4' }, { type: 2, foo: '5' }, ], }), ).rejects.toThrowError(/must be a `number` type/); }); describe('addMethod', () => { it('extending Schema should make method accessible everywhere', () => { addMethod(Schema, 'foo', () => 'here'); expect(string().foo()).toBe('here'); }); test.each([ ['mixed', mixed], ['object', object], ['array', array], ['string', string], ['number', number], ['boolean', boolean], ['date', date], ])('should work with factories: %s', (_msg, factory) => { addMethod(factory, 'foo', () => 'here'); expect(factory().foo()).toBe('here'); }); test.each([ ['mixed', MixedSchema], ['object', ObjectSchema], ['array', ArraySchema], ['string', StringSchema], ['number', NumberSchema], ['boolean', BooleanSchema], ['date', DateSchema], ])('should work with classes: %s', (_msg, ctor) => { addMethod(ctor, 'foo', () => 'here'); expect(new ctor().foo()).toBe('here'); }); }); }); ================================================ FILE: test-setup.mjs ================================================ import { beforeAll } from 'vitest'; import { SynchronousPromise } from 'synchronous-promise'; import * as yup from './src/index.ts'; beforeAll(() => { if (global.YUP_USE_SYNC) { const { Schema } = yup; const { validateSync } = Schema.prototype; Schema.prototype.validate = function (value, options = {}) { return new SynchronousPromise((resolve, reject) => { let result; try { result = validateSync.call(this, value, options); } catch (err) { reject(err); } resolve(result); }); }; } }); ================================================ FILE: tsconfig.json ================================================ { "extends": "@4c/tsconfig/web", "compilerOptions": { "rootDir": "src", "strictFunctionTypes": true }, "include": ["src/**/*.ts"] } ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', setupFiles: ['./test-setup.mjs'], include: ['test/**/*.{js,ts}'], exclude: [ 'test/helpers.ts', 'test/.eslintrc.js', 'test/**/.eslintrc.js', 'test/types/types.ts', ], globals: false, projects: [ { extends: true, test: { name: 'async', }, define: { 'global.YUP_USE_SYNC': false, }, }, { extends: true, test: { name: 'sync', }, define: { 'global.YUP_USE_SYNC': true, }, }, ], }, });