@warlock.js/seal 5.29.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +58 -8
- package/cjs/index.cjs +58 -43
- package/cjs/index.cjs.map +1 -1
- package/cjs/{object-DubGC-xu.cjs → object-VTtA-k1a.cjs} +23 -19
- package/cjs/object-VTtA-k1a.cjs.map +1 -0
- package/cjs/object.cjs +2 -2
- package/cjs/{required-methods-BOVNRtci.cjs → required-methods-chT0TXQS.cjs} +197 -92
- package/cjs/required-methods-chT0TXQS.cjs.map +1 -0
- package/cjs/{string-DpDpgefH.cjs → string-edcPIpcU.cjs} +8 -8
- package/cjs/string-edcPIpcU.cjs.map +1 -0
- package/cjs/string.cjs +2 -2
- package/esm/config.d.mts +2 -1
- package/esm/config.d.mts.map +1 -1
- package/esm/config.mjs +1 -1
- package/esm/config.mjs.map +1 -1
- package/esm/factory/validate.d.mts +9 -6
- package/esm/factory/validate.d.mts.map +1 -1
- package/esm/factory/validate.mjs +12 -4
- package/esm/factory/validate.mjs.map +1 -1
- package/esm/factory/validators.d.mts +13 -13
- package/esm/factory/validators.d.mts.map +1 -1
- package/esm/factory/validators.mjs +8 -2
- package/esm/factory/validators.mjs.map +1 -1
- package/esm/helpers/date-helpers.d.mts +1 -1
- package/esm/helpers/date-helpers.d.mts.map +1 -1
- package/esm/helpers/date-helpers.mjs +1 -1
- package/esm/helpers/date-helpers.mjs.map +1 -1
- package/esm/helpers/file.utils.d.mts +1 -1
- package/esm/helpers/file.utils.d.mts.map +1 -1
- package/esm/helpers/file.utils.mjs +1 -1
- package/esm/helpers/file.utils.mjs.map +1 -1
- package/esm/helpers/get-field-value.d.mts +2 -1
- package/esm/helpers/get-field-value.d.mts.map +1 -1
- package/esm/helpers/get-field-value.mjs +1 -1
- package/esm/helpers/get-field-value.mjs.map +1 -1
- package/esm/helpers/is-empty-value.mjs +1 -1
- package/esm/helpers/is-empty-value.mjs.map +1 -1
- package/esm/helpers/path-helpers.d.mts +1 -1
- package/esm/helpers/path-helpers.d.mts.map +1 -1
- package/esm/helpers/path-helpers.mjs +1 -1
- package/esm/helpers/path-helpers.mjs.map +1 -1
- package/esm/helpers/validation-helpers.d.mts +2 -1
- package/esm/helpers/validation-helpers.d.mts.map +1 -1
- package/esm/helpers/validation-helpers.mjs +1 -1
- package/esm/helpers/validation-helpers.mjs.map +1 -1
- package/esm/index.d.mts +3 -3
- package/esm/mutators/array-mutators.d.mts +2 -1
- package/esm/mutators/array-mutators.d.mts.map +1 -1
- package/esm/mutators/array-mutators.mjs +1 -1
- package/esm/mutators/array-mutators.mjs.map +1 -1
- package/esm/mutators/boolean-mutators.d.mts +2 -1
- package/esm/mutators/boolean-mutators.d.mts.map +1 -1
- package/esm/mutators/boolean-mutators.mjs +1 -1
- package/esm/mutators/boolean-mutators.mjs.map +1 -1
- package/esm/mutators/date-mutators.d.mts +2 -1
- package/esm/mutators/date-mutators.d.mts.map +1 -1
- package/esm/mutators/date-mutators.mjs +1 -1
- package/esm/mutators/date-mutators.mjs.map +1 -1
- package/esm/mutators/number-mutators.d.mts +2 -1
- package/esm/mutators/number-mutators.d.mts.map +1 -1
- package/esm/mutators/number-mutators.mjs +1 -1
- package/esm/mutators/number-mutators.mjs.map +1 -1
- package/esm/mutators/object-mutators.d.mts +2 -1
- package/esm/mutators/object-mutators.d.mts.map +1 -1
- package/esm/mutators/object-mutators.mjs +1 -1
- package/esm/mutators/object-mutators.mjs.map +1 -1
- package/esm/mutators/scalar-mutators.d.mts +2 -1
- package/esm/mutators/scalar-mutators.d.mts.map +1 -1
- package/esm/mutators/scalar-mutators.mjs +1 -1
- package/esm/mutators/scalar-mutators.mjs.map +1 -1
- package/esm/mutators/string-mutators.d.mts +2 -1
- package/esm/mutators/string-mutators.d.mts.map +1 -1
- package/esm/mutators/string-mutators.mjs +3 -3
- package/esm/mutators/string-mutators.mjs.map +1 -1
- package/esm/object.d.mts +2 -4
- package/esm/object.d.mts.map +1 -1
- package/esm/object.mjs +1 -1
- package/esm/object.mjs.map +1 -1
- package/esm/plugins/plugin-system.d.mts +1 -1
- package/esm/plugins/plugin-system.d.mts.map +1 -1
- package/esm/plugins/plugin-system.mjs +1 -1
- package/esm/plugins/plugin-system.mjs.map +1 -1
- package/esm/rules/array/array-rules.d.mts +2 -1
- package/esm/rules/array/array-rules.d.mts.map +1 -1
- package/esm/rules/array/array-rules.mjs +1 -1
- package/esm/rules/array/array-rules.mjs.map +1 -1
- package/esm/rules/color/color-rules.d.mts +2 -1
- package/esm/rules/color/color-rules.d.mts.map +1 -1
- package/esm/rules/color/color-rules.mjs +1 -1
- package/esm/rules/color/color-rules.mjs.map +1 -1
- package/esm/rules/common/enum.d.mts +2 -1
- package/esm/rules/common/enum.d.mts.map +1 -1
- package/esm/rules/common/enum.mjs +1 -1
- package/esm/rules/common/enum.mjs.map +1 -1
- package/esm/rules/common/equals-field-rules.d.mts +2 -1
- package/esm/rules/common/equals-field-rules.d.mts.map +1 -1
- package/esm/rules/common/equals-field-rules.mjs +1 -1
- package/esm/rules/common/equals-field-rules.mjs.map +1 -1
- package/esm/rules/common/instanceof.d.mts +2 -1
- package/esm/rules/common/instanceof.d.mts.map +1 -1
- package/esm/rules/common/instanceof.mjs +1 -1
- package/esm/rules/common/instanceof.mjs.map +1 -1
- package/esm/rules/common/literal.d.mts +2 -1
- package/esm/rules/common/literal.d.mts.map +1 -1
- package/esm/rules/common/literal.mjs +1 -1
- package/esm/rules/common/literal.mjs.map +1 -1
- package/esm/rules/common/type-rules.d.mts +2 -1
- package/esm/rules/common/type-rules.d.mts.map +1 -1
- package/esm/rules/common/type-rules.mjs +1 -1
- package/esm/rules/common/type-rules.mjs.map +1 -1
- package/esm/rules/common/unknown-key.d.mts +2 -1
- package/esm/rules/common/unknown-key.d.mts.map +1 -1
- package/esm/rules/common/unknown-key.mjs +1 -1
- package/esm/rules/common/unknown-key.mjs.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.d.mts +2 -1
- package/esm/rules/conditional/forbidden-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.mjs +1 -1
- package/esm/rules/conditional/forbidden-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-if-rules.d.mts +2 -1
- package/esm/rules/conditional/present-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-if-rules.mjs +1 -1
- package/esm/rules/conditional/present-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-unless-rules.d.mts +2 -1
- package/esm/rules/conditional/present-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-unless-rules.mjs +1 -1
- package/esm/rules/conditional/present-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-with-rules.d.mts +2 -1
- package/esm/rules/conditional/present-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-with-rules.mjs +1 -1
- package/esm/rules/conditional/present-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-without-rules.d.mts +2 -1
- package/esm/rules/conditional/present-without-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-without-rules.mjs +1 -1
- package/esm/rules/conditional/present-without-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-if-rules.d.mts +2 -1
- package/esm/rules/conditional/required-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-if-rules.mjs +1 -1
- package/esm/rules/conditional/required-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-unless-rules.d.mts +2 -1
- package/esm/rules/conditional/required-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-unless-rules.mjs +1 -1
- package/esm/rules/conditional/required-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-when-rule.d.mts +2 -1
- package/esm/rules/conditional/required-when-rule.d.mts.map +1 -1
- package/esm/rules/conditional/required-when-rule.mjs +1 -1
- package/esm/rules/conditional/required-when-rule.mjs.map +1 -1
- package/esm/rules/conditional/required-with-rules.d.mts +2 -1
- package/esm/rules/conditional/required-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-with-rules.mjs +1 -1
- package/esm/rules/conditional/required-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-without-rules.d.mts +2 -1
- package/esm/rules/conditional/required-without-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-without-rules.mjs +1 -1
- package/esm/rules/conditional/required-without-rules.mjs.map +1 -1
- package/esm/rules/core/equal.d.mts +2 -1
- package/esm/rules/core/equal.d.mts.map +1 -1
- package/esm/rules/core/equal.mjs +1 -1
- package/esm/rules/core/equal.mjs.map +1 -1
- package/esm/rules/core/forbidden.d.mts +2 -1
- package/esm/rules/core/forbidden.d.mts.map +1 -1
- package/esm/rules/core/forbidden.mjs +1 -1
- package/esm/rules/core/forbidden.mjs.map +1 -1
- package/esm/rules/core/required.d.mts +2 -1
- package/esm/rules/core/required.d.mts.map +1 -1
- package/esm/rules/core/required.mjs +1 -1
- package/esm/rules/core/required.mjs.map +1 -1
- package/esm/rules/core/union.d.mts +9 -2
- package/esm/rules/core/union.d.mts.map +1 -1
- package/esm/rules/core/union.mjs +59 -11
- package/esm/rules/core/union.mjs.map +1 -1
- package/esm/rules/core/when.d.mts +2 -1
- package/esm/rules/core/when.d.mts.map +1 -1
- package/esm/rules/core/when.mjs +1 -1
- package/esm/rules/core/when.mjs.map +1 -1
- package/esm/rules/date/date-comparison-rules.d.mts +2 -1
- package/esm/rules/date/date-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-comparison-rules.mjs +1 -1
- package/esm/rules/date/date-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-day-rules.d.mts +2 -1
- package/esm/rules/date/date-day-rules.d.mts.map +1 -1
- package/esm/rules/date/date-day-rules.mjs +1 -1
- package/esm/rules/date/date-day-rules.mjs.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.d.mts +2 -1
- package/esm/rules/date/date-field-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.mjs +1 -1
- package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-period-rules.d.mts +2 -1
- package/esm/rules/date/date-period-rules.d.mts.map +1 -1
- package/esm/rules/date/date-period-rules.mjs +1 -1
- package/esm/rules/date/date-period-rules.mjs.map +1 -1
- package/esm/rules/date/date-relative-rules.d.mts +2 -1
- package/esm/rules/date/date-relative-rules.d.mts.map +1 -1
- package/esm/rules/date/date-relative-rules.mjs +1 -1
- package/esm/rules/date/date-relative-rules.mjs.map +1 -1
- package/esm/rules/date/date-special-rules.d.mts +2 -1
- package/esm/rules/date/date-special-rules.d.mts.map +1 -1
- package/esm/rules/date/date-special-rules.mjs +1 -1
- package/esm/rules/date/date-special-rules.mjs.map +1 -1
- package/esm/rules/date/date.d.mts +2 -1
- package/esm/rules/date/date.d.mts.map +1 -1
- package/esm/rules/date/date.mjs +1 -1
- package/esm/rules/date/date.mjs.map +1 -1
- package/esm/rules/file/dimensions.d.mts +2 -1
- package/esm/rules/file/dimensions.d.mts.map +1 -1
- package/esm/rules/file/dimensions.mjs +1 -1
- package/esm/rules/file/dimensions.mjs.map +1 -1
- package/esm/rules/file/file-size.d.mts +2 -1
- package/esm/rules/file/file-size.d.mts.map +1 -1
- package/esm/rules/file/file-size.mjs +1 -1
- package/esm/rules/file/file-size.mjs.map +1 -1
- package/esm/rules/length/length-rules.d.mts +2 -1
- package/esm/rules/length/length-rules.d.mts.map +1 -1
- package/esm/rules/length/length-rules.mjs +1 -1
- package/esm/rules/length/length-rules.mjs.map +1 -1
- package/esm/rules/number/number-rules.d.mts +2 -1
- package/esm/rules/number/number-rules.d.mts.map +1 -1
- package/esm/rules/number/number-rules.mjs +1 -1
- package/esm/rules/number/number-rules.mjs.map +1 -1
- package/esm/rules/scalar/accepted-rule.mjs +1 -1
- package/esm/rules/scalar/accepted-rule.mjs.map +1 -1
- package/esm/rules/scalar/declined-rule.mjs +1 -1
- package/esm/rules/scalar/declined-rule.mjs.map +1 -1
- package/esm/rules/string/alpha.d.mts +2 -1
- package/esm/rules/string/alpha.d.mts.map +1 -1
- package/esm/rules/string/alpha.mjs +1 -1
- package/esm/rules/string/alpha.mjs.map +1 -1
- package/esm/rules/string/credit-card.d.mts +2 -1
- package/esm/rules/string/credit-card.d.mts.map +1 -1
- package/esm/rules/string/credit-card.mjs +1 -1
- package/esm/rules/string/credit-card.mjs.map +1 -1
- package/esm/rules/string/email.d.mts +2 -1
- package/esm/rules/string/email.d.mts.map +1 -1
- package/esm/rules/string/email.mjs +1 -1
- package/esm/rules/string/email.mjs.map +1 -1
- package/esm/rules/string/id-formats.d.mts +2 -1
- package/esm/rules/string/id-formats.d.mts.map +1 -1
- package/esm/rules/string/id-formats.mjs +1 -1
- package/esm/rules/string/id-formats.mjs.map +1 -1
- package/esm/rules/string/ip.d.mts +2 -1
- package/esm/rules/string/ip.d.mts.map +1 -1
- package/esm/rules/string/ip.mjs +1 -1
- package/esm/rules/string/ip.mjs.map +1 -1
- package/esm/rules/string/matches.d.mts +2 -1
- package/esm/rules/string/matches.d.mts.map +1 -1
- package/esm/rules/string/matches.mjs +1 -1
- package/esm/rules/string/matches.mjs.map +1 -1
- package/esm/rules/string/pattern.d.mts +2 -1
- package/esm/rules/string/pattern.d.mts.map +1 -1
- package/esm/rules/string/pattern.mjs +1 -1
- package/esm/rules/string/pattern.mjs.map +1 -1
- package/esm/rules/string/string-comparison.d.mts +2 -1
- package/esm/rules/string/string-comparison.d.mts.map +1 -1
- package/esm/rules/string/string-comparison.mjs +1 -1
- package/esm/rules/string/string-comparison.mjs.map +1 -1
- package/esm/rules/string/strong-password-rule.d.mts +2 -1
- package/esm/rules/string/strong-password-rule.d.mts.map +1 -1
- package/esm/rules/string/strong-password-rule.mjs +1 -1
- package/esm/rules/string/strong-password-rule.mjs.map +1 -1
- package/esm/rules/string/url.d.mts +2 -1
- package/esm/rules/string/url.d.mts.map +1 -1
- package/esm/rules/string/url.mjs +1 -1
- package/esm/rules/string/url.mjs.map +1 -1
- package/esm/rules/string/without-whitespace.d.mts +2 -1
- package/esm/rules/string/without-whitespace.d.mts.map +1 -1
- package/esm/rules/string/without-whitespace.mjs +1 -1
- package/esm/rules/string/without-whitespace.mjs.map +1 -1
- package/esm/standard-schema/json-schema.d.mts +1 -1
- package/esm/standard-schema/json-schema.d.mts.map +1 -1
- package/esm/standard-schema/json-schema.mjs +1 -1
- package/esm/standard-schema/json-schema.mjs.map +1 -1
- package/esm/standard-schema/map-result.d.mts +2 -1
- package/esm/standard-schema/map-result.d.mts.map +1 -1
- package/esm/standard-schema/map-result.mjs +1 -1
- package/esm/standard-schema/map-result.mjs.map +1 -1
- package/esm/standard-schema/types.d.mts +1 -1
- package/esm/standard-schema/types.d.mts.map +1 -1
- package/esm/string.d.mts +1 -1
- package/esm/string.d.mts.map +1 -1
- package/esm/string.mjs +1 -1
- package/esm/string.mjs.map +1 -1
- package/esm/types/conditional-types.d.mts +1 -1
- package/esm/types/conditional-types.d.mts.map +1 -1
- package/esm/types/context-types.d.mts +8 -2
- package/esm/types/context-types.d.mts.map +1 -1
- package/esm/types/data-transformer-types.d.mts +1 -1
- package/esm/types/data-transformer-types.d.mts.map +1 -1
- package/esm/types/date-types.d.mts +1 -1
- package/esm/types/date-types.d.mts.map +1 -1
- package/esm/types/date-types.mjs +1 -1
- package/esm/types/date-types.mjs.map +1 -1
- package/esm/types/file.types.d.mts +1 -1
- package/esm/types/file.types.d.mts.map +1 -1
- package/esm/types/inference-types.d.mts +22 -5
- package/esm/types/inference-types.d.mts.map +1 -1
- package/esm/types/mutator-types.d.mts +1 -1
- package/esm/types/mutator-types.d.mts.map +1 -1
- package/esm/types/result-types.d.mts +26 -2
- package/esm/types/result-types.d.mts.map +1 -1
- package/esm/types/rule-types.d.mts +11 -2
- package/esm/types/rule-types.d.mts.map +1 -1
- package/esm/types/schema-types.d.mts +1 -1
- package/esm/types/schema-types.d.mts.map +1 -1
- package/esm/validators/any-validator.d.mts +1 -1
- package/esm/validators/any-validator.d.mts.map +1 -1
- package/esm/validators/any-validator.mjs +1 -1
- package/esm/validators/any-validator.mjs.map +1 -1
- package/esm/validators/array-validator.d.mts +1 -1
- package/esm/validators/array-validator.d.mts.map +1 -1
- package/esm/validators/array-validator.mjs +4 -4
- package/esm/validators/array-validator.mjs.map +1 -1
- package/esm/validators/base-validator.d.mts +10 -3
- package/esm/validators/base-validator.d.mts.map +1 -1
- package/esm/validators/base-validator.mjs +67 -18
- package/esm/validators/base-validator.mjs.map +1 -1
- package/esm/validators/boolean-validator.d.mts +1 -1
- package/esm/validators/boolean-validator.d.mts.map +1 -1
- package/esm/validators/boolean-validator.mjs +1 -1
- package/esm/validators/boolean-validator.mjs.map +1 -1
- package/esm/validators/computed-validator.d.mts +1 -1
- package/esm/validators/computed-validator.d.mts.map +1 -1
- package/esm/validators/computed-validator.mjs +5 -5
- package/esm/validators/computed-validator.mjs.map +1 -1
- package/esm/validators/date-validator.d.mts +2 -1
- package/esm/validators/date-validator.d.mts.map +1 -1
- package/esm/validators/date-validator.mjs +1 -1
- package/esm/validators/date-validator.mjs.map +1 -1
- package/esm/validators/discriminated-union-validator.d.mts +8 -2
- package/esm/validators/discriminated-union-validator.d.mts.map +1 -1
- package/esm/validators/discriminated-union-validator.mjs +7 -2
- package/esm/validators/discriminated-union-validator.mjs.map +1 -1
- package/esm/validators/float-validator.d.mts +1 -1
- package/esm/validators/float-validator.d.mts.map +1 -1
- package/esm/validators/float-validator.mjs +1 -1
- package/esm/validators/float-validator.mjs.map +1 -1
- package/esm/validators/instanceof-validator.d.mts +1 -1
- package/esm/validators/instanceof-validator.d.mts.map +1 -1
- package/esm/validators/instanceof-validator.mjs +1 -1
- package/esm/validators/instanceof-validator.mjs.map +1 -1
- package/esm/validators/int-validator.d.mts +1 -1
- package/esm/validators/int-validator.d.mts.map +1 -1
- package/esm/validators/int-validator.mjs +1 -1
- package/esm/validators/int-validator.mjs.map +1 -1
- package/esm/validators/lazy-validator.d.mts +1 -1
- package/esm/validators/lazy-validator.d.mts.map +1 -1
- package/esm/validators/lazy-validator.mjs +2 -2
- package/esm/validators/lazy-validator.mjs.map +1 -1
- package/esm/validators/literal-validator.d.mts +1 -1
- package/esm/validators/literal-validator.d.mts.map +1 -1
- package/esm/validators/literal-validator.mjs +5 -2
- package/esm/validators/literal-validator.mjs.map +1 -1
- package/esm/validators/managed-validator.d.mts +1 -1
- package/esm/validators/managed-validator.d.mts.map +1 -1
- package/esm/validators/managed-validator.mjs +1 -1
- package/esm/validators/managed-validator.mjs.map +1 -1
- package/esm/validators/methods/equality-conditional-methods.d.mts +2 -1
- package/esm/validators/methods/equality-conditional-methods.d.mts.map +1 -1
- package/esm/validators/methods/equality-conditional-methods.mjs +1 -1
- package/esm/validators/methods/equality-conditional-methods.mjs.map +1 -1
- package/esm/validators/methods/forbidden-methods.d.mts +1 -1
- package/esm/validators/methods/forbidden-methods.d.mts.map +1 -1
- package/esm/validators/methods/forbidden-methods.mjs +1 -1
- package/esm/validators/methods/forbidden-methods.mjs.map +1 -1
- package/esm/validators/methods/present-methods.d.mts +1 -1
- package/esm/validators/methods/present-methods.d.mts.map +1 -1
- package/esm/validators/methods/present-methods.mjs +1 -1
- package/esm/validators/methods/present-methods.mjs.map +1 -1
- package/esm/validators/methods/required-methods.d.mts +2 -1
- package/esm/validators/methods/required-methods.d.mts.map +1 -1
- package/esm/validators/methods/required-methods.mjs +1 -1
- package/esm/validators/methods/required-methods.mjs.map +1 -1
- package/esm/validators/number-validator.d.mts +1 -1
- package/esm/validators/number-validator.d.mts.map +1 -1
- package/esm/validators/number-validator.mjs +1 -1
- package/esm/validators/number-validator.mjs.map +1 -1
- package/esm/validators/numeric-validator.d.mts +1 -1
- package/esm/validators/numeric-validator.d.mts.map +1 -1
- package/esm/validators/numeric-validator.mjs +1 -1
- package/esm/validators/numeric-validator.mjs.map +1 -1
- package/esm/validators/object-validator.d.mts +1 -1
- package/esm/validators/object-validator.d.mts.map +1 -1
- package/esm/validators/object-validator.mjs +14 -10
- package/esm/validators/object-validator.mjs.map +1 -1
- package/esm/validators/primitive-validator.d.mts +1 -1
- package/esm/validators/primitive-validator.d.mts.map +1 -1
- package/esm/validators/primitive-validator.mjs +1 -1
- package/esm/validators/primitive-validator.mjs.map +1 -1
- package/esm/validators/record-validator.d.mts +1 -1
- package/esm/validators/record-validator.d.mts.map +1 -1
- package/esm/validators/record-validator.mjs +4 -4
- package/esm/validators/record-validator.mjs.map +1 -1
- package/esm/validators/scalar-validator.d.mts +1 -1
- package/esm/validators/scalar-validator.d.mts.map +1 -1
- package/esm/validators/scalar-validator.mjs +1 -1
- package/esm/validators/scalar-validator.mjs.map +1 -1
- package/esm/validators/string-validator.d.mts +1 -1
- package/esm/validators/string-validator.d.mts.map +1 -1
- package/esm/validators/string-validator.mjs +1 -1
- package/esm/validators/string-validator.mjs.map +1 -1
- package/esm/validators/tuple-validator.d.mts +1 -1
- package/esm/validators/tuple-validator.d.mts.map +1 -1
- package/esm/validators/tuple-validator.mjs +4 -4
- package/esm/validators/tuple-validator.mjs.map +1 -1
- package/esm/validators/union-validator.d.mts +4 -2
- package/esm/validators/union-validator.d.mts.map +1 -1
- package/esm/validators/union-validator.mjs +4 -2
- package/esm/validators/union-validator.mjs.map +1 -1
- package/llms-full.txt +664 -533
- package/llms.txt +1 -1
- package/package.json +3 -2
- package/skills/bridge-standard-schema/SKILL.md +60 -36
- package/skills/compose-seal-modifiers/SKILL.md +89 -53
- package/skills/define-structural-shape/SKILL.md +41 -27
- package/skills/define-structural-shape/array-methods.md +27 -27
- package/skills/define-structural-shape/object-methods.md +38 -30
- package/skills/extend-seal-with-plugins/SKILL.md +39 -5
- package/skills/generate-json-schema/SKILL.md +13 -13
- package/skills/handle-seal-errors/SKILL.md +26 -28
- package/skills/index.md +4 -1
- package/skills/overview/SKILL.md +10 -2
- package/skills/pick-seal-primitive/SKILL.md +49 -45
- package/skills/pick-seal-primitive/date-methods.md +96 -96
- package/skills/pick-seal-primitive/number-methods.md +55 -55
- package/skills/pick-seal-primitive/string-methods.md +107 -102
- package/skills/seal-basics/SKILL.md +15 -15
- package/cjs/object-DubGC-xu.cjs.map +0 -1
- package/cjs/required-methods-BOVNRtci.cjs.map +0 -1
- package/cjs/string-DpDpgefH.cjs.map +0 -1
- package/esm/helpers/index.d.mts +0 -5
- package/esm/mutators/index.d.mts +0 -7
- package/esm/rules/array/index.d.mts +0 -1
- package/esm/rules/color/index.d.mts +0 -1
- package/esm/rules/common/index.d.mts +0 -6
- package/esm/rules/conditional/index.d.mts +0 -10
- package/esm/rules/core/index.d.mts +0 -5
- package/esm/rules/date/index.d.mts +0 -7
- package/esm/rules/file/index.d.mts +0 -2
- package/esm/rules/index.d.mts +0 -45
- package/esm/rules/length/index.d.mts +0 -1
- package/esm/rules/number/index.d.mts +0 -1
- package/esm/rules/string/index.d.mts +0 -11
- package/esm/standard-schema/index.d.mts +0 -3
- package/esm/types/index.d.mts +0 -10
package/llms-full.txt
CHANGED
|
@@ -8,35 +8,36 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: bridge-standard-schema
|
|
11
|
-
description: 'Diagnose `StandardSchemaV1` interop —
|
|
11
|
+
description: 'Diagnose `StandardSchemaV1` interop — input/output class parameters and primitive factory intersections, why typed slots reject a schema, cascade `Model<TSchema>` variance. Triggers: `StandardSchemaV1`, `~standard`, `Infer`, `ObjectValidator`, `StringValidator`, `LiteralValidator`, `BaseValidator`, `Model<TSchema>`, `StandardJSONSchemaV1`, `Result<unknown>`; "StandardSchemaV1 slot rejecting my schema", "Result<unknown> error", "drop as unknown as cast", "cascade Model schema variance"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: foundations — the `seal-basics` topic; JSON Schema gen — the `generate-json-schema` topic; competing spec `@standard-schema/spec`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# The Standard Schema bridge
|
|
15
15
|
|
|
16
16
|
Seal validators implement [Standard Schema V1](https://standardschema.dev) — every validator exposes a `~standard` member with `validate()`, JSON Schema metadata, and inferred types. Any consumer that accepts `StandardSchemaV1<T>` (`@warlock.js/ai` supervisor `output`, ai tool `input`, LangGraph state, OpenAI structured outputs, TanStack Form, Conform) accepts a seal schema directly.
|
|
17
17
|
|
|
18
|
-
This skill
|
|
18
|
+
This skill covers the input and output types exposed through `~standard`, and
|
|
19
|
+
compatibility with consumers such as Cascade model schemas and AI tool schemas.
|
|
19
20
|
|
|
20
21
|
## The shape of the bridge
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
`ObjectValidator<TSchema>` already threads `Infer.Input` and `Infer.Output` shapes
|
|
24
|
+
into `BaseValidator<TInput, TOutput>`. Both `v.object(fields)` and the standalone
|
|
25
|
+
`object(fields)` factory return that class directly. No extra Standard Schema
|
|
26
|
+
intersection is needed for objects: avoiding one keeps large composed schemas
|
|
27
|
+
cheaper for TypeScript to instantiate.
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
object: <T extends Schema>(schema: T) =>
|
|
27
|
-
new ObjectValidator<T>(schema) as ObjectValidator<T>
|
|
28
|
-
& StandardSchemaV1<Infer<ObjectValidator<T>>>;
|
|
29
|
-
|
|
30
|
-
string: () =>
|
|
31
|
-
new StringValidator() as StringValidator & StandardSchemaV1<string>;
|
|
32
|
-
|
|
33
|
-
literal: <T extends readonly [...] >(...values: T) =>
|
|
34
|
-
new LiteralValidator<T>(values) as LiteralValidator<T> & StandardSchemaV1<T[number]>;
|
|
29
|
+
Other factories add a Standard Schema intersection when their class has no precise
|
|
30
|
+
generic output declaration, for example:
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
```ts
|
|
33
|
+
// Inside Seal's factory:
|
|
34
|
+
string: () => new StringValidator() as StringValidator & StandardSchemaV1<string>;
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
Keep input and output distinct when writing consumer types. Defaults and catches
|
|
38
|
+
can make input fields optional while output fields are required. Avoid computing
|
|
39
|
+
`Infer<this>` in the base class: object shapes are already supplied as generic
|
|
40
|
+
parameters, and recursively walking the full class increases compiler work.
|
|
40
41
|
|
|
41
42
|
## What this means for app code
|
|
42
43
|
|
|
@@ -58,7 +59,7 @@ ai.tool({
|
|
|
58
59
|
name: "create_user",
|
|
59
60
|
description: "Create a new user",
|
|
60
61
|
input: userSchema,
|
|
61
|
-
execute: async input => createUser(input),
|
|
62
|
+
execute: async (input) => createUser(input),
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
// ✅ supervisor output — no cast
|
|
@@ -82,7 +83,7 @@ const schema = v.object({
|
|
|
82
83
|
blocks: v.array(v.literal("text", "image")).optional(),
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
type Output = Infer<typeof schema>;
|
|
86
|
+
type Output = Infer.Output<typeof schema>;
|
|
86
87
|
// { email: string; blocks?: ("text" | "image")[] }
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -92,17 +93,12 @@ Hand-rolled parallel types drift the moment a field changes — and seal's tight
|
|
|
92
93
|
|
|
93
94
|
Three failure modes you'll see in real projects:
|
|
94
95
|
|
|
95
|
-
### 1.
|
|
96
|
+
### 1. An annotation discarded schema details
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// ✅ Let inference run
|
|
102
|
-
const schema = v.object({...});
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The annotation strips `& StandardSchemaV1<...>` from the type. The schema stops fitting `StandardSchemaV1<T>` slots. Fix: remove the annotation. If you need the value type, use `Infer<typeof schema>`.
|
|
98
|
+
Let the factory infer its concrete schema. An annotation such as bare
|
|
99
|
+
`ObjectValidator` loses the field types; primitive factories also lose their
|
|
100
|
+
Standard Schema intersection when annotated with a bare primitive class.
|
|
101
|
+
`ObjectValidator<typeof fields>` preserves an object's input and output types.
|
|
106
102
|
|
|
107
103
|
### 2. The schema's inferred shape doesn't match the target type
|
|
108
104
|
|
|
@@ -117,23 +113,51 @@ The supervisor's explicit `<MyOutput>` generic is a constraint the schema must s
|
|
|
117
113
|
- **Drop the explicit generic** — let the supervisor infer output type from the schema. The hand-rolled type was probably documentation-only anyway.
|
|
118
114
|
- **Align them** — if the hand-rolled type is the source of truth (e.g. a domain type from elsewhere), make the schema match.
|
|
119
115
|
|
|
120
|
-
### 3. The error says `Result<unknown>`
|
|
116
|
+
### 3. The error says `Result<unknown>`
|
|
117
|
+
|
|
118
|
+
Check whether an annotation erased a factory's output type, and compare
|
|
119
|
+
`Infer.Input<typeof schema>` and `Infer.Output<typeof schema>` with the consumer's
|
|
120
|
+
expected types. Output slots should use the post-default output shape. Objects
|
|
121
|
+
carry that shape directly through their class generics; primitive factories add
|
|
122
|
+
it through their Standard Schema intersection.
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
## Union and record outputs
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
Use `Infer.Output<T>` or `StandardSchemaV1.InferOutput<T>` for downstream data.
|
|
127
|
+
Unions and records preserve their children's output types independently of input
|
|
128
|
+
shapes. Defaults and catches make object fields required in output. Their factory
|
|
129
|
+
intersections put the precise Standard Schema signature first, so direct
|
|
130
|
+
`~standard.validate()` calls return the declared output. `Infer.Output` reads the
|
|
131
|
+
output metadata rather than inferring from overloaded validation signatures. A union
|
|
132
|
+
returns the selected branch's validated data before its own transformers and
|
|
133
|
+
refinements; it does not discard branch defaults, computed values or transformations.
|
|
125
134
|
|
|
126
135
|
```ts
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
136
|
+
const entry = v.object({ count: v.int().optional().default(1) });
|
|
137
|
+
const entries = v.record(entry);
|
|
138
|
+
type Entries = Infer.Output<typeof entries>;
|
|
139
|
+
// Record<string, { count: number }>
|
|
140
|
+
const maybeEntry = v.union([entry, v.string()]);
|
|
141
|
+
type MaybeEntry = Infer.Output<typeof maybeEntry>;
|
|
142
|
+
// { count: number } | string
|
|
130
143
|
```
|
|
131
144
|
|
|
132
|
-
|
|
145
|
+
When upgrading, union output can change through member trimming, Date parsing,
|
|
146
|
+
defaults, computed fields and unknown-key stripping. An explicit `undefined`
|
|
147
|
+
member output drops its containing object field. Keep the raw input before
|
|
148
|
+
validation if storage, comparisons or serialization also need that value.
|
|
149
|
+
|
|
150
|
+
## Compiler budget
|
|
151
|
+
|
|
152
|
+
From `packages/seal`, run `pnpm bench:types --check` after changes to inference or
|
|
153
|
+
factory types. CI runs the same deterministic composed-schema fixture when Seal
|
|
154
|
+
is selected. Instantiations and type counts may grow at most 5% above the checked-in
|
|
155
|
+
baseline. Refresh with `node scripts/bench-types.mjs --update` only after reviewing
|
|
156
|
+
the measurements; time and memory are diagnostic rather than gated.
|
|
133
157
|
|
|
134
158
|
## Why not `Omit<class, "~standard"> & StandardSchemaV1<T>`?
|
|
135
159
|
|
|
136
|
-
Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors.
|
|
160
|
+
Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors. Keep object factories as their existing generic class, and retain factory-side intersections for primitive classes that need them. Both preserve the validator's class members for generic consumers.
|
|
137
161
|
|
|
138
162
|
## Why not import `@standard-schema/spec` instead of forking the types?
|
|
139
163
|
|
|
@@ -167,31 +191,61 @@ When `validate()` runs against a value, the order is:
|
|
|
167
191
|
3. optional/required check — decide whether to run rules
|
|
168
192
|
4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
|
|
169
193
|
5. rules — every other rule, in declaration order
|
|
170
|
-
6.
|
|
194
|
+
6. children — validate container members, fill defaults and computed fields
|
|
195
|
+
7. transformers — reshape value into data (.toISOString, .toLowerCase)
|
|
196
|
+
8. refinements — typed custom checks against successful output
|
|
171
197
|
```
|
|
172
198
|
|
|
173
199
|
If a rule fails, transformers don't run for that field.
|
|
174
200
|
|
|
175
201
|
**Mutator vs transformer mental model:**
|
|
202
|
+
|
|
176
203
|
- **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
|
|
177
204
|
- **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
|
|
178
205
|
|
|
179
206
|
```ts
|
|
180
207
|
// trim BEFORE length check — use a mutator
|
|
181
|
-
v.string()
|
|
208
|
+
v.string()
|
|
209
|
+
.addMutator((s) => s.trim())
|
|
210
|
+
.min(3);
|
|
182
211
|
// " Hi " → mutator trims → "Hi" → fails min(3)
|
|
183
212
|
|
|
184
213
|
// trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
|
|
185
|
-
v.string().min(3).trim()
|
|
214
|
+
v.string().min(3).trim();
|
|
186
215
|
// " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
|
|
187
216
|
```
|
|
188
217
|
|
|
218
|
+
## `.refine()`
|
|
219
|
+
|
|
220
|
+
Refinements receive the validator's output type, after children, defaults, computed
|
|
221
|
+
fields and output transformers. They skip failed validation and absent/null values,
|
|
222
|
+
including when `firstErrorOnly: false`. Return an error string to reject the value;
|
|
223
|
+
return `undefined` to accept it. Async callbacks are supported. Use `.addRule()`
|
|
224
|
+
when a check must run against input before children validate.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
v.object({ count: v.int().optional().default(1) }).refine((value) => {
|
|
228
|
+
// count is a number, and defaults have already been applied.
|
|
229
|
+
return value.count > 10 ? "Too many items" : undefined;
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Successful empty defaults such as `.default("")` are refined too. `.merge()`
|
|
234
|
+
appends the other object's refinements; `.extend()` imports only fields. Cloning
|
|
235
|
+
and field-selection methods preserve the receiver's refinements, so callbacks
|
|
236
|
+
must tolerate fields removed by `.pick()` or `.without()`.
|
|
237
|
+
|
|
238
|
+
Output transformers do not yet change declared output types: a
|
|
239
|
+
`v.date().toISOString()` refinement receives a string at runtime despite the
|
|
240
|
+
`Date` declaration. Use a Date input rule, or convert after validation; moving
|
|
241
|
+
`.refine()` earlier in the chain does not move it before transformers.
|
|
242
|
+
|
|
189
243
|
## `.required()` / `.optional()` / `.present()`
|
|
190
244
|
|
|
191
245
|
```ts
|
|
192
|
-
v.string()
|
|
193
|
-
v.string().optional()
|
|
194
|
-
v.string().present()
|
|
246
|
+
v.string(); // already required inside v.object
|
|
247
|
+
v.string().optional(); // type: string | undefined — { isOptional: true } brand
|
|
248
|
+
v.string().present(); // must exist, may be "" / null
|
|
195
249
|
```
|
|
196
250
|
|
|
197
251
|
**Required is the default inside `v.object`.** Most schemas read cleaner without `.required()` — `Infer<>` already shows what's required (no `?`) vs optional (`?`). Calling `.required()` explicitly is harmless and accepted, but the canonical seal style is to skip it. Keep `.optional()` explicit (it changes behavior); skip `.required()` (it doesn't).
|
|
@@ -199,9 +253,11 @@ v.string().present() // must exist, may be "" / null
|
|
|
199
253
|
The `{ isOptional: true }` brand survives chaining (`.optional().min(3)` is still optional) and `Infer<>` reads it to make the key optional.
|
|
200
254
|
|
|
201
255
|
**When `.required()` is still useful:**
|
|
202
|
-
|
|
256
|
+
|
|
257
|
+
- It's not needed for conditional rules — `.requiredIf(field, value)` and friends _replace_ the default required-condition slot, so they work standalone.
|
|
203
258
|
|
|
204
259
|
Conditional variants (run inside `v.object` only):
|
|
260
|
+
|
|
205
261
|
- `.requiredIf(field, value)` / `.requiredIfSibling(field, value)`
|
|
206
262
|
- `.requiredWith(field)` / `.requiredWithSibling(field)`
|
|
207
263
|
- `.requiredWithout(field)` / `.requiredWithoutSibling(field)`
|
|
@@ -213,22 +269,22 @@ Conditional variants (run inside `v.object` only):
|
|
|
213
269
|
## `.nullable()` / `.notNullable()` / `.nullish()`
|
|
214
270
|
|
|
215
271
|
```ts
|
|
216
|
-
v.string().nullable()
|
|
217
|
-
v.string().nullish()
|
|
272
|
+
v.string().nullable(); // type: string | null
|
|
273
|
+
v.string().nullish(); // sugar for .optional().nullable()
|
|
218
274
|
```
|
|
219
275
|
|
|
220
|
-
Independent of optional — a field can be required
|
|
276
|
+
Independent of optional — a field can be required _and_ nullable. Defaults to non-nullable. The `{ isNullable: true }` brand widens both `Infer.Input` and `Infer.Output` with `| null`.
|
|
221
277
|
|
|
222
278
|
## `.default(value | callback)`
|
|
223
279
|
|
|
224
280
|
```ts
|
|
225
|
-
v.string().default("guest")
|
|
226
|
-
v.int().default(0)
|
|
227
|
-
v.date().default(() => new Date())
|
|
228
|
-
v.array(v.string()).default([])
|
|
281
|
+
v.string().default("guest");
|
|
282
|
+
v.int().default(0);
|
|
283
|
+
v.date().default(() => new Date()); // lazy — fresh each validation
|
|
284
|
+
v.array(v.string()).default([]);
|
|
229
285
|
```
|
|
230
286
|
|
|
231
|
-
If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires
|
|
287
|
+
If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires _before_ rules — so `v.string().min(3).default("a")` fails because `"a"` is shorter than 3.
|
|
232
288
|
|
|
233
289
|
Date sugar: `v.date().defaultNow()` ≡ `.default(() => new Date())`.
|
|
234
290
|
|
|
@@ -236,7 +292,7 @@ The `{ hasDefault: true }` brand makes the key optional in `Infer.Input` (caller
|
|
|
236
292
|
|
|
237
293
|
## `.catch(fallback)`
|
|
238
294
|
|
|
239
|
-
Rescues
|
|
295
|
+
Rescues _failed_ validation by substituting a fallback. Complement to `.default()`:
|
|
240
296
|
|
|
241
297
|
- `.default(x)` fires when input is **absent**
|
|
242
298
|
- `.catch(y)` fires when input is **present but invalid**
|
|
@@ -271,7 +327,7 @@ const schema = v.object({
|
|
|
271
327
|
retries: v.int().catch(3),
|
|
272
328
|
});
|
|
273
329
|
|
|
274
|
-
type In
|
|
330
|
+
type In = Infer.Input<typeof schema>;
|
|
275
331
|
// {
|
|
276
332
|
// bio?: string;
|
|
277
333
|
// status?: Status; ← default makes caller optional
|
|
@@ -285,10 +341,11 @@ type Out = Infer.Output<typeof schema>;
|
|
|
285
341
|
// retries: number; ← catch guarantees a value
|
|
286
342
|
// }
|
|
287
343
|
|
|
288
|
-
type Default = Infer<typeof schema>;
|
|
344
|
+
type Default = Infer<typeof schema>; // alias for Infer.Input
|
|
289
345
|
```
|
|
290
346
|
|
|
291
347
|
**When to reach for which:**
|
|
348
|
+
|
|
292
349
|
- `Infer.Input<T>` (or bare `Infer<T>`) — for HTTP request bodies, form payloads, DTOs, anything pre-validation. **The common case in HTTP-shaped code.**
|
|
293
350
|
- `Infer.Output<T>` — for Cascade `Model<>` params, validated state, anywhere downstream of `validate()`.
|
|
294
351
|
|
|
@@ -298,26 +355,26 @@ Both widen with `| null` when `.nullable()` is set.
|
|
|
298
355
|
|
|
299
356
|
Three failure modes, three different rescue mechanisms:
|
|
300
357
|
|
|
301
|
-
| Input state
|
|
302
|
-
|
|
|
303
|
-
| Field absent
|
|
304
|
-
| Field absent (no default)
|
|
305
|
-
| Field present and invalid
|
|
306
|
-
| Field is `null`
|
|
307
|
-
| Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is
|
|
358
|
+
| Input state | Rescued by | What appears in `data` |
|
|
359
|
+
| --------------------------------------- | -------------------------------------- | ---------------------- |
|
|
360
|
+
| Field absent | `.default(x)` | `x` |
|
|
361
|
+
| Field absent (no default) | `.optional()` | Key omitted entirely |
|
|
362
|
+
| Field present and invalid | `.catch(y)` | `y` |
|
|
363
|
+
| Field is `null` | `.nullable()` | `null` |
|
|
364
|
+
| Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
|
|
308
365
|
|
|
309
366
|
Full truth table for an `.optional()` field inside `v.object(…)`:
|
|
310
367
|
|
|
311
|
-
| Input
|
|
312
|
-
|
|
|
313
|
-
| Field absent
|
|
314
|
-
| Field is explicit `undefined`
|
|
315
|
-
| Field is `null` (no `.nullable()`)
|
|
316
|
-
| Field is `null` with `.nullable()`
|
|
317
|
-
| Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent
|
|
318
|
-
| Field is `.default(x)` and absent
|
|
319
|
-
| Field is `.default(x)` and present
|
|
320
|
-
| Field is `.catch(y)` and validation fails
|
|
368
|
+
| Input | What appears in `data` |
|
|
369
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
370
|
+
| Field absent | Key **omitted entirely** (not `undefined`-valued) |
|
|
371
|
+
| Field is explicit `undefined` | Key omitted (treated identically to absent) |
|
|
372
|
+
| Field is `null` (no `.nullable()`) | Key omitted — for an **optional** field, `null` coalesces to empty and the (cleared) required rule doesn't fire. On a **required** field the same `null` triggers a validation error. |
|
|
373
|
+
| Field is `null` with `.nullable()` | Key is `null` |
|
|
374
|
+
| Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
|
|
375
|
+
| Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
|
|
376
|
+
| Field is `.default(x)` and present | Caller value wins; default is unused |
|
|
377
|
+
| Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
|
|
321
378
|
|
|
322
379
|
```ts
|
|
323
380
|
const schema = v.object({
|
|
@@ -325,11 +382,12 @@ const schema = v.object({
|
|
|
325
382
|
embedding: v.array(v.number()).optional(),
|
|
326
383
|
});
|
|
327
384
|
|
|
328
|
-
(await validate(schema, {}))
|
|
329
|
-
|
|
330
|
-
//
|
|
385
|
+
(await validate(schema, {}))
|
|
386
|
+
.data
|
|
387
|
+
// → {} — neither key appears
|
|
388
|
+
// (NOT { metadata: {}, embedding: [] })
|
|
331
389
|
|
|
332
|
-
(await validate(schema, { metadata: {}, embedding: [] })).data
|
|
390
|
+
(await validate(schema, { metadata: {}, embedding: [] })).data;
|
|
333
391
|
// → { metadata: {}, embedding: [] } — present-empty is preserved
|
|
334
392
|
```
|
|
335
393
|
|
|
@@ -342,12 +400,12 @@ Collection validators (`v.record`, `v.array`, `v.tuple`) explicitly honor this
|
|
|
342
400
|
Available on `v.string()`, `v.number()`, `v.int()`, `v.float()`, `v.boolean()`, `v.scalar()`:
|
|
343
401
|
|
|
344
402
|
```ts
|
|
345
|
-
v.string().in(["admin", "user", "guest"])
|
|
346
|
-
v.string().oneOf(["a", "b"])
|
|
347
|
-
v.string().notIn(["banned", "blocked"])
|
|
348
|
-
v.string().forbids(["banned"])
|
|
349
|
-
v.number().allowsOnly([1, 2, 3])
|
|
350
|
-
v.string().enum(MyTSEnum)
|
|
403
|
+
v.string().in(["admin", "user", "guest"]); // value must match one
|
|
404
|
+
v.string().oneOf(["a", "b"]); // alias for .in
|
|
405
|
+
v.string().notIn(["banned", "blocked"]); // value must NOT match
|
|
406
|
+
v.string().forbids(["banned"]); // alias for .notIn
|
|
407
|
+
v.number().allowsOnly([1, 2, 3]); // stricter — explicit allowlist
|
|
408
|
+
v.string().enum(MyTSEnum); // accepts a TS enum object via Object.values
|
|
351
409
|
```
|
|
352
410
|
|
|
353
411
|
For literal-typed narrowing (`"admin" | "user" | "guest"` instead of `string`), use `v.literal(...)` instead — `oneOf` keeps the broader primitive type.
|
|
@@ -359,10 +417,10 @@ v.object({
|
|
|
359
417
|
email: v.string(),
|
|
360
418
|
password: v.string(),
|
|
361
419
|
passwordConfirm: v.string().sameAs("password").omit(),
|
|
362
|
-
})
|
|
420
|
+
});
|
|
363
421
|
```
|
|
364
422
|
|
|
365
|
-
`.omit()` keeps the field in
|
|
423
|
+
`.omit()` keeps the field in _validation_ but drops it from `data` and from `Infer<>`. Use for confirmation/checksum fields that exist only for cross-field rules. `.exclude()` is the same idea, used internally for managed/computed plumbing.
|
|
366
424
|
|
|
367
425
|
## `.label("Display Name")` — field display name
|
|
368
426
|
|
|
@@ -371,7 +429,7 @@ To control the `:input` placeholder in a field's own messages, call `.label()` o
|
|
|
371
429
|
```ts
|
|
372
430
|
v.object({
|
|
373
431
|
email_address: v.string().label("Email Address"),
|
|
374
|
-
})
|
|
432
|
+
});
|
|
375
433
|
// Error message: "The Email Address is required" (instead of "The email_address is required")
|
|
376
434
|
```
|
|
377
435
|
|
|
@@ -379,12 +437,14 @@ v.object({
|
|
|
379
437
|
|
|
380
438
|
### `.attributes({ ... })` is a different tool
|
|
381
439
|
|
|
382
|
-
`.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference
|
|
440
|
+
`.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference _other_ fields (e.g. `matches`):
|
|
383
441
|
|
|
384
442
|
```ts
|
|
385
|
-
v.string()
|
|
386
|
-
|
|
387
|
-
|
|
443
|
+
v.string()
|
|
444
|
+
.sameAs("confirmPassword")
|
|
445
|
+
.attributes({
|
|
446
|
+
matches: { confirmPassword: "Confirm Password" },
|
|
447
|
+
});
|
|
388
448
|
```
|
|
389
449
|
|
|
390
450
|
For per-field display names use `.label()`; for translated messages wire the `translateRule` / `translateAttribute` hooks via `configureSeal()`.
|
|
@@ -412,7 +472,7 @@ Switch back with `.immutable`. Default is fine 99% of the time. Reach for `.muta
|
|
|
412
472
|
|
|
413
473
|
## Things NOT to do
|
|
414
474
|
|
|
415
|
-
- Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming
|
|
475
|
+
- Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming _before_ `.min()`, use `.addMutator(s => s.trim())`.
|
|
416
476
|
- Don't combine `.required()` and `.optional()` on the same chain — last wins, but the intent is unclear; pick one.
|
|
417
477
|
- Don't chain `.default("a")` with `.min(3)` and expect `"a"` to pass — the default goes through rules.
|
|
418
478
|
- Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
|
|
@@ -437,20 +497,20 @@ v.object({
|
|
|
437
497
|
email: v.string().email(),
|
|
438
498
|
age: v.int().min(13).optional(),
|
|
439
499
|
role: v.literal("admin", "user", "guest"),
|
|
440
|
-
})
|
|
500
|
+
});
|
|
441
501
|
```
|
|
442
502
|
|
|
443
503
|
- **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
|
|
444
504
|
- **Unknown keys policy.** By default extra keys are silently dropped from `data`. Toggle with `.allowUnknown()` (forward as-is), `.stripUnknown()` (explicit drop), or `.allow(...keys)` (whitelist specific extras). See [`object-methods.md`](./object-methods.md).
|
|
445
505
|
- **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
|
|
446
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields
|
|
506
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields _inside_ a `v.object`. Without a parent, sibling resolution silently passes.
|
|
447
507
|
|
|
448
508
|
## `v.array` — homogeneous lists
|
|
449
509
|
|
|
450
510
|
```ts
|
|
451
|
-
v.array(v.string())
|
|
452
|
-
v.array(userSchema)
|
|
453
|
-
v.array(v.array(v.int()))
|
|
511
|
+
v.array(v.string()); // type: string[]
|
|
512
|
+
v.array(userSchema); // type: User[]
|
|
513
|
+
v.array(v.array(v.int())); // type: number[][] — recursive inference
|
|
454
514
|
```
|
|
455
515
|
|
|
456
516
|
The inner validator runs against each element; failure on any element fails the array. Method surface (`.min`, `.max`, `.unique`, `.sorted`, `.flip`, …) in [`array-methods.md`](./array-methods.md).
|
|
@@ -458,9 +518,9 @@ The inner validator runs against each element; failure on any element fails the
|
|
|
458
518
|
## `v.record` — homogeneous values, dynamic keys
|
|
459
519
|
|
|
460
520
|
```ts
|
|
461
|
-
v.record(v.int())
|
|
462
|
-
v.record(v.object({ count: v.int() }))
|
|
463
|
-
v.record()
|
|
521
|
+
v.record(v.int()); // type: Record<string, number>
|
|
522
|
+
v.record(v.object({ count: v.int() })); // type: Record<string, { count: number }>
|
|
523
|
+
v.record(); // type: Record<string, any>
|
|
464
524
|
```
|
|
465
525
|
|
|
466
526
|
Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but values share a schema. If keys are also constrained (e.g. only `"draft" | "published"`), use `v.object` with literal keys instead — the constraint lives in the type.
|
|
@@ -468,26 +528,40 @@ Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but
|
|
|
468
528
|
## `v.tuple` — positional types
|
|
469
529
|
|
|
470
530
|
```ts
|
|
471
|
-
v.tuple([v.string(), v.int(), v.boolean()])
|
|
472
|
-
v.tuple([v.literal("ok"), v.string()])
|
|
531
|
+
v.tuple([v.string(), v.int(), v.boolean()]); // type: [string, number, boolean]
|
|
532
|
+
v.tuple([v.literal("ok"), v.string()]); // type: ["ok", string]
|
|
473
533
|
```
|
|
474
534
|
|
|
475
535
|
Each position has its own validator; the array length must match the tuple length. Pair with `v.literal` at position 0 for result-tuple patterns (`["ok", data]` vs `["error", message]`).
|
|
476
536
|
|
|
537
|
+
Pass the validators as a literal list so each position keeps its own type, for input and validated output alike. A list typed `BaseValidator[]` (built at runtime) has no fixed positions, so its output is an array rather than a tuple.
|
|
538
|
+
|
|
477
539
|
## `v.union` — one of N validators (untagged)
|
|
478
540
|
|
|
479
541
|
```ts
|
|
480
|
-
v.union([v.string(), v.int()])
|
|
542
|
+
v.union([v.string(), v.int()]); // type: string | number
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Every branch whose type matches the value (`matchesType()`) is tried, in order, and the first that passes wins; the union returns that branch's validated output. `firstErrorOnly` limits the errors reported, never the branches tried, so a union of several object shapes accepts a value matching any of them:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
const owner = v.object({ kind: v.literal("owner"), id: v.literal("__owner__") });
|
|
549
|
+
const member = v.object({ kind: v.literal("member"), id: v.string() });
|
|
550
|
+
|
|
551
|
+
await validate(v.union([owner, member]), { kind: "member", id: "x" });
|
|
552
|
+
// → valid, data: { kind: "member", id: "x" }
|
|
481
553
|
```
|
|
482
554
|
|
|
483
|
-
|
|
555
|
+
When no branch passes, the union reports the **closest** failed branch, picked by: fewest mismatched `v.literal(...)` fields, then fewest errors, then the deepest error path, then declaration order. Its errors keep their own paths (`actor.id`, not `actor`) and have the type `"union"` — the first one by default, all of them with `firstErrorOnly: false`. A custom `errorMessage` (`v.union([...], "Unknown actor")`), or a value no branch can hold by type, gives a single error at the union's path.
|
|
556
|
+
|
|
557
|
+
For object shapes that share a tag field, `v.discriminatedUnion` is still the better fit: it routes straight to one branch instead of trying each, and narrows exactly in TypeScript.
|
|
484
558
|
|
|
485
559
|
## `v.discriminatedUnion` — tagged unions (recommended for objects)
|
|
486
560
|
|
|
487
561
|
```ts
|
|
488
562
|
const email = v.object({ type: v.literal("email"), email: v.string().email() });
|
|
489
|
-
const sms
|
|
490
|
-
const push
|
|
563
|
+
const sms = v.object({ type: v.literal("sms"), phone: v.string() });
|
|
564
|
+
const push = v.object({ type: v.literal("push"), deviceId: v.string() });
|
|
491
565
|
|
|
492
566
|
const notif = v.discriminatedUnion("type", [email, sms, push]);
|
|
493
567
|
|
|
@@ -499,8 +573,8 @@ type Notif = Infer<typeof notif>;
|
|
|
499
573
|
|
|
500
574
|
Routes payloads by reading the discriminator field (here `type`), looking it up in a key→branch map built at construction time, and delegating to the matching branch only. Benefits over plain `v.union`:
|
|
501
575
|
|
|
502
|
-
- **Precise errors.** Failures come from the
|
|
503
|
-
- **O(1) routing** instead of
|
|
576
|
+
- **Precise errors.** Failures come from the branch the tag names, not from the closest guess.
|
|
577
|
+
- **O(1) routing** instead of trying each branch.
|
|
504
578
|
- **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
|
|
505
579
|
- **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
|
|
506
580
|
|
|
@@ -538,20 +612,20 @@ Use for: trees (categories, file systems), threaded data (comment chains), mutua
|
|
|
538
612
|
|
|
539
613
|
## Quick map
|
|
540
614
|
|
|
541
|
-
| Want
|
|
542
|
-
|
|
|
543
|
-
| Fixed-shape record
|
|
544
|
-
| Dynamic keys, same value shape
|
|
545
|
-
| List of items
|
|
546
|
-
| Position-typed array
|
|
547
|
-
| One of N scalar types
|
|
548
|
-
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])`
|
|
549
|
-
| Self-referencing or forward reference
|
|
550
|
-
| One of N constants
|
|
615
|
+
| Want | Reach for |
|
|
616
|
+
| --------------------------------------- | ----------------------------------------------------------------------------- |
|
|
617
|
+
| Fixed-shape record | `v.object({...})` |
|
|
618
|
+
| Dynamic keys, same value shape | `v.record(valueSchema)` |
|
|
619
|
+
| List of items | `v.array(itemSchema)` |
|
|
620
|
+
| Position-typed array | `v.tuple([a, b, c])` |
|
|
621
|
+
| One of N scalar types | `v.union([...])` |
|
|
622
|
+
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
|
|
623
|
+
| Self-referencing or forward reference | `v.lazy(() => schema)` |
|
|
624
|
+
| One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
|
|
551
625
|
|
|
552
626
|
## A note on cascade Models
|
|
553
627
|
|
|
554
|
-
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains
|
|
628
|
+
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains _"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"_, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See the `bridge-standard-schema` topic.
|
|
555
629
|
|
|
556
630
|
## Method-surface reference
|
|
557
631
|
|
|
@@ -569,41 +643,41 @@ The inner validator runs against each element. Failure on any element fails the
|
|
|
569
643
|
|
|
570
644
|
## Length
|
|
571
645
|
|
|
572
|
-
| Method
|
|
573
|
-
|
|
574
|
-
| `.min(n, msg?)`
|
|
575
|
-
| `.minLength(n, msg?)`
|
|
576
|
-
| `.maxLength(n, msg?)`
|
|
577
|
-
| `.length(n, msg?)`
|
|
578
|
-
| `.between(a, b, msg?)`
|
|
579
|
-
| `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | —
|
|
646
|
+
| Method | Args | JSON Schema | Example |
|
|
647
|
+
| ---------------------------- | ---------------------- | -------------------------- | ------------------------------------ |
|
|
648
|
+
| `.min(n, msg?)` | n/a — see note below | — | use `.minLength(n)` |
|
|
649
|
+
| `.minLength(n, msg?)` | inclusive lower bound | `minItems: n` | `v.array(v.string()).minLength(1)` |
|
|
650
|
+
| `.maxLength(n, msg?)` | inclusive upper bound | `maxItems: n` | `v.array(v.string()).maxLength(10)` |
|
|
651
|
+
| `.length(n, msg?)` | exact length | `minItems=maxItems=n` | `v.array(v.string()).length(3)` |
|
|
652
|
+
| `.between(a, b, msg?)` | inclusive range | `minItems: a, maxItems: b` | `v.array(v.string()).between(1, 10)` |
|
|
653
|
+
| `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | — |
|
|
580
654
|
|
|
581
655
|
**Note on `.min`/`.max`:** the array validator does not expose `.min`/`.max` directly — use `.minLength`/`.maxLength` (or `.between`/`.length`). This is intentional: `.min`/`.max` would conflict with primitive value-comparison semantics if they ever bled into here.
|
|
582
656
|
|
|
583
657
|
## Uniqueness & sort
|
|
584
658
|
|
|
585
|
-
| Method
|
|
586
|
-
|
|
587
|
-
| `.unique(msg?)`
|
|
659
|
+
| Method | Args | Effect |
|
|
660
|
+
| --------------------------- | ----------------------------- | ----------------------------- |
|
|
661
|
+
| `.unique(msg?)` | — | every element is distinct |
|
|
588
662
|
| `.sorted(direction?, msg?)` | `"asc"` (default) or `"desc"` | array is monotonically sorted |
|
|
589
663
|
|
|
590
664
|
## Mutators (pre-validation reshape)
|
|
591
665
|
|
|
592
|
-
| Method
|
|
593
|
-
|
|
594
|
-
| `.flip()`
|
|
595
|
-
| `.reverse()`
|
|
596
|
-
| `.onlyUnique()`
|
|
597
|
-
| `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation
|
|
666
|
+
| Method | Args | Effect |
|
|
667
|
+
| ------------------------- | ----------------------------------------------------------------- | ------------------------ |
|
|
668
|
+
| `.flip()` | — | reverse the array |
|
|
669
|
+
| `.reverse()` | — | alias for `.flip()` |
|
|
670
|
+
| `.onlyUnique()` | — | dedupe before validation |
|
|
671
|
+
| `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation |
|
|
598
672
|
|
|
599
673
|
`onlyUnique()` and `unique()` differ: the mutator silently dedupes; the rule fails when duplicates are present. Pick by intent — coerce vs reject.
|
|
600
674
|
|
|
601
675
|
## Inner validator notes
|
|
602
676
|
|
|
603
677
|
```ts
|
|
604
|
-
v.array(v.string().email())
|
|
605
|
-
v.array(userSchema)
|
|
606
|
-
v.array(v.array(v.int()))
|
|
678
|
+
v.array(v.string().email()); // every element must be a valid email
|
|
679
|
+
v.array(userSchema); // every element must satisfy userSchema
|
|
680
|
+
v.array(v.array(v.int())); // matrix-ish — Infer is recursive
|
|
607
681
|
```
|
|
608
682
|
|
|
609
683
|
Element validators run with element-level context. `path` becomes `${parent}.${index}` for error messages. Cross-field rules inside element schemas resolve to that element's fields, not the array.
|
|
@@ -621,25 +695,25 @@ Element validators run with element-level context. `path` becomes `${parent}.${i
|
|
|
621
695
|
|
|
622
696
|
```ts
|
|
623
697
|
// Tags
|
|
624
|
-
v.array(v.string().min(1)).unique().between(1, 10)
|
|
698
|
+
v.array(v.string().min(1)).unique().between(1, 10);
|
|
625
699
|
|
|
626
700
|
// User list
|
|
627
|
-
v.array(userSchema).minLength(1)
|
|
701
|
+
v.array(userSchema).minLength(1);
|
|
628
702
|
|
|
629
703
|
// Pre-sorted, deduped
|
|
630
|
-
v.array(v.string()).onlyUnique().sort("asc")
|
|
704
|
+
v.array(v.string()).onlyUnique().sort("asc");
|
|
631
705
|
|
|
632
706
|
// Reverse-emit
|
|
633
|
-
v.array(v.string()).flip()
|
|
707
|
+
v.array(v.string()).flip();
|
|
634
708
|
|
|
635
709
|
// Matrix
|
|
636
|
-
v.array(v.array(v.number())).minLength(1)
|
|
710
|
+
v.array(v.array(v.number())).minLength(1);
|
|
637
711
|
|
|
638
712
|
// Sorted ids
|
|
639
|
-
v.array(v.string().uuid()).sorted("asc").unique()
|
|
713
|
+
v.array(v.string().uuid()).sorted("asc").unique();
|
|
640
714
|
|
|
641
715
|
// Optional list with default
|
|
642
|
-
v.array(v.string()).default([]).optional()
|
|
716
|
+
v.array(v.string()).default([]).optional();
|
|
643
717
|
```
|
|
644
718
|
|
|
645
719
|
|
|
@@ -656,10 +730,12 @@ These return a new `ObjectValidator` with a transformed schema. The TypeScript i
|
|
|
656
730
|
### `.extend(schemaOrValidator)` — add fields, keep config
|
|
657
731
|
|
|
658
732
|
```ts
|
|
659
|
-
const baseUser = v
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
733
|
+
const baseUser = v
|
|
734
|
+
.object({
|
|
735
|
+
name: v.string(),
|
|
736
|
+
email: v.string().email(),
|
|
737
|
+
})
|
|
738
|
+
.allowUnknown();
|
|
663
739
|
|
|
664
740
|
const adminUser = baseUser.extend({
|
|
665
741
|
role: v.string().oneOf(["admin", "superadmin"]),
|
|
@@ -673,10 +749,12 @@ Accepts a plain schema object **or** another `ObjectValidator` (only its schema
|
|
|
673
749
|
|
|
674
750
|
```ts
|
|
675
751
|
const base = v.object({ name: v.string() }).allowUnknown();
|
|
676
|
-
const audit = v
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
752
|
+
const audit = v
|
|
753
|
+
.object({
|
|
754
|
+
createdAt: v.date(),
|
|
755
|
+
updatedAt: v.date(),
|
|
756
|
+
})
|
|
757
|
+
.stripUnknown();
|
|
680
758
|
|
|
681
759
|
const merged = base.merge(audit);
|
|
682
760
|
// type: { name; createdAt; updatedAt }
|
|
@@ -713,8 +791,8 @@ Returns `ObjectValidator<Omit<TSchema, K>>`. Inverse of `.pick`. Keeps original
|
|
|
713
791
|
### `.partial(...keys?)` — mark fields optional
|
|
714
792
|
|
|
715
793
|
```ts
|
|
716
|
-
fullUser.partial()
|
|
717
|
-
fullUser.partial("password")
|
|
794
|
+
fullUser.partial(); // every field becomes optional
|
|
795
|
+
fullUser.partial("password"); // only `password` becomes optional
|
|
718
796
|
```
|
|
719
797
|
|
|
720
798
|
Walks the schema and applies `.optional()` to each named field (or all if no keys given). `Infer<>` makes those keys optional.
|
|
@@ -722,7 +800,7 @@ Walks the schema and applies `.optional()` to each named field (or all if no key
|
|
|
722
800
|
### `.requiredFields(...keys?)` — mark fields required
|
|
723
801
|
|
|
724
802
|
```ts
|
|
725
|
-
const partialUser = fullUser.partial();
|
|
803
|
+
const partialUser = fullUser.partial(); // all optional
|
|
726
804
|
const updateUser = partialUser.requiredFields("id", "email");
|
|
727
805
|
// id and email are required again, others stay optional
|
|
728
806
|
```
|
|
@@ -733,29 +811,31 @@ Inverse of `.partial`.
|
|
|
733
811
|
|
|
734
812
|
By default, extra keys in input are silently dropped from `data` (no error, no forward).
|
|
735
813
|
|
|
736
|
-
| Method
|
|
737
|
-
|
|
738
|
-
| `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is
|
|
739
|
-
| `.stripUnknown()`
|
|
740
|
-
| `.allow(...keys)`
|
|
814
|
+
| Method | Effect |
|
|
815
|
+
| ----------------------------- | ------------------------------------------------------- |
|
|
816
|
+
| `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is |
|
|
817
|
+
| `.stripUnknown()` | explicit drop (mutator-based — affects `data` shape) |
|
|
818
|
+
| `.allow(...keys)` | whitelist specific extras to forward without validation |
|
|
741
819
|
|
|
742
820
|
`.allowUnknown()` only affects direct children — nested objects keep their own policies. For a fully permissive object including nested children, set `.allowUnknown()` on each level.
|
|
743
821
|
|
|
744
822
|
### Output DTOs — stripping extras instead of rejecting them
|
|
745
823
|
|
|
746
|
-
The default is to **reject** unknown keys, which is what you want for inbound payloads. For an
|
|
824
|
+
The default is to **reject** unknown keys, which is what you want for inbound payloads. For an _outbound_ DTO — where the record legitimately carries internal fields you simply don't want to expose — use `.stripUnknown()`, which drops them and returns the clean object:
|
|
747
825
|
|
|
748
826
|
```ts
|
|
749
|
-
const publicArticle = v
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
827
|
+
const publicArticle = v
|
|
828
|
+
.object({
|
|
829
|
+
id: v.string(),
|
|
830
|
+
title: v.string(),
|
|
831
|
+
})
|
|
832
|
+
.stripUnknown();
|
|
753
833
|
|
|
754
834
|
const { isValid, data } = await v.validate(publicArticle, record);
|
|
755
835
|
// record had version / authorId / status; data has only id + title
|
|
756
836
|
```
|
|
757
837
|
|
|
758
|
-
Without `.stripUnknown()` the same record fails with
|
|
838
|
+
Without `.stripUnknown()` the same record fails with _"The schema contains unknown keys: …"_ rather than being cleaned.
|
|
759
839
|
|
|
760
840
|
:::note[`data` is `undefined` on failure — since 4.9.2]
|
|
761
841
|
A failed validation returns `data: undefined`, never the input it rejected. Before 4.9.2 an `object` returned the raw input on failure (while `discriminatedUnion` returned `undefined`), so a caller who read `data` without branching on `isValid` forwarded the exact fields the schema existed to exclude. Always branch on `isValid`.
|
|
@@ -763,8 +843,8 @@ A failed validation returns `data: undefined`, never the input it rejected. Befo
|
|
|
763
843
|
|
|
764
844
|
## Object-level mutators
|
|
765
845
|
|
|
766
|
-
| Method
|
|
767
|
-
|
|
846
|
+
| Method | Args | Effect |
|
|
847
|
+
| ------------------- | -------------- | ------------------------------------------------ |
|
|
768
848
|
| `.trim(recursive?)` | default `true` | recursively trim string values across the object |
|
|
769
849
|
|
|
770
850
|
For per-field mutators, attach on the field validator instead.
|
|
@@ -777,7 +857,7 @@ The cross-field methods (`.sameAs`, `.requiredIf`, `.requiredWith`, etc.) are **
|
|
|
777
857
|
v.object({
|
|
778
858
|
password: v.string(),
|
|
779
859
|
passwordConfirm: v.string().sameAs("password"),
|
|
780
|
-
})
|
|
860
|
+
});
|
|
781
861
|
```
|
|
782
862
|
|
|
783
863
|
If you call cross-field rules outside a `v.object`, sibling resolution silently passes — there's nobody to compare against.
|
|
@@ -785,7 +865,7 @@ If you call cross-field rules outside a `v.object`, sibling resolution silently
|
|
|
785
865
|
## `Infer<>` semantics
|
|
786
866
|
|
|
787
867
|
```ts
|
|
788
|
-
Infer<typeof userSchema
|
|
868
|
+
Infer<typeof userSchema>;
|
|
789
869
|
// { reqField: T; optField?: T }
|
|
790
870
|
```
|
|
791
871
|
|
|
@@ -818,10 +898,12 @@ const timestamps = v.object({
|
|
|
818
898
|
const userWithAudit = baseUser.extend(timestamps);
|
|
819
899
|
|
|
820
900
|
// Permissive container that forwards extras
|
|
821
|
-
const eventEnvelope = v
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}).allowUnknown()
|
|
901
|
+
const eventEnvelope = v
|
|
902
|
+
.object({
|
|
903
|
+
type: v.literal("user.created", "user.updated"),
|
|
904
|
+
payload: v.object({}).allowUnknown(),
|
|
905
|
+
})
|
|
906
|
+
.allowUnknown();
|
|
825
907
|
|
|
826
908
|
// Confirmation pattern with omit
|
|
827
909
|
const signupSchema = v.object({
|
|
@@ -858,7 +940,7 @@ Seal exposes a plugin system so you can add validator methods without forking th
|
|
|
858
940
|
import type { SealPlugin } from "@warlock.js/seal";
|
|
859
941
|
|
|
860
942
|
type SealPlugin = {
|
|
861
|
-
name: string;
|
|
943
|
+
name: string; // unique identifier — duplicates warn and skip install
|
|
862
944
|
version?: string;
|
|
863
945
|
description?: string;
|
|
864
946
|
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
@@ -896,8 +978,8 @@ import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from
|
|
|
896
978
|
await registerPlugin(slugPlugin);
|
|
897
979
|
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
898
980
|
|
|
899
|
-
hasPlugin("slug");
|
|
900
|
-
getInstalledPlugins();
|
|
981
|
+
hasPlugin("slug"); // → true
|
|
982
|
+
getInstalledPlugins(); // → [slugPlugin]
|
|
901
983
|
|
|
902
984
|
await unregisterPlugin("slug");
|
|
903
985
|
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
@@ -1000,8 +1082,8 @@ For a method that creates a **new validator** (not chained from an existing one)
|
|
|
1000
1082
|
## Introspection — checking what's loaded
|
|
1001
1083
|
|
|
1002
1084
|
```ts
|
|
1003
|
-
hasPlugin("slug");
|
|
1004
|
-
getInstalledPlugins();
|
|
1085
|
+
hasPlugin("slug"); // boolean
|
|
1086
|
+
getInstalledPlugins(); // SealPlugin[]
|
|
1005
1087
|
```
|
|
1006
1088
|
|
|
1007
1089
|
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
@@ -1014,6 +1096,40 @@ Use these in startup diagnostics or in tests that need to assert a plugin is reg
|
|
|
1014
1096
|
- Don't ship a plugin that overrides a built-in method without a clear reason. If you must, `uninstall` should restore the original — but the better path is a different method name.
|
|
1015
1097
|
- Don't author one plugin per method. Group related methods (e.g. "country-specific validators") into one plugin so the install/uninstall lifecycle is coherent.
|
|
1016
1098
|
|
|
1099
|
+
## Returning validated rule output
|
|
1100
|
+
|
|
1101
|
+
A custom rule's success result can contain `data` to replace the current value:
|
|
1102
|
+
`{ isValid: true, data: replacement }`. Later rules, transformers and refinements
|
|
1103
|
+
receive that replacement. Omitting `data` preserves the current value; a present
|
|
1104
|
+
`data: undefined` explicitly clears it. This is also how a union preserves the
|
|
1105
|
+
selected branch's validated defaults, catches and child transformations.
|
|
1106
|
+
|
|
1107
|
+
## Reporting several errors from one rule
|
|
1108
|
+
|
|
1109
|
+
A failed rule result can carry `errors: [{ error, path }, ...]` to report more than
|
|
1110
|
+
one error, each at its own path; every entry gets the rule's `name` as its `type`.
|
|
1111
|
+
`v.union` uses this to relay the closest branch's errors. Return as many as fit the
|
|
1112
|
+
caller's `context.configurations?.firstErrorOnly` (default `true`: one).
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
import { invalidRule, type SchemaRule } from "@warlock.js/seal";
|
|
1116
|
+
|
|
1117
|
+
const bothDates: SchemaRule = {
|
|
1118
|
+
name: "bothDates",
|
|
1119
|
+
async validate(value: { from?: unknown; to?: unknown }, context) {
|
|
1120
|
+
const missing = (["from", "to"] as const).filter((key) => !value?.[key]);
|
|
1121
|
+
if (missing.length === 0) return { isValid: true };
|
|
1122
|
+
|
|
1123
|
+
const failed = invalidRule(this, context);
|
|
1124
|
+
const errors = missing.map((key) => ({
|
|
1125
|
+
error: `The ${key} date is required`,
|
|
1126
|
+
path: context.path ? `${context.path}.${key}` : key,
|
|
1127
|
+
}));
|
|
1128
|
+
return { ...failed, errors };
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1017
1133
|
|
|
1018
1134
|
## generate-json-schema `@warlock.js/seal/generate-json-schema/SKILL.md`
|
|
1019
1135
|
|
|
@@ -1048,19 +1164,19 @@ userSchema.toJsonSchema("draft-2020-12");
|
|
|
1048
1164
|
|
|
1049
1165
|
```ts
|
|
1050
1166
|
type JsonSchemaTarget =
|
|
1051
|
-
| "draft-2020-12"
|
|
1052
|
-
| "draft-07"
|
|
1053
|
-
| "openapi-3.0"
|
|
1054
|
-
| "openai-strict"
|
|
1167
|
+
| "draft-2020-12" // default — modern JSON Schema
|
|
1168
|
+
| "draft-07" // older tooling, Swagger 2.0
|
|
1169
|
+
| "openapi-3.0" // uses { nullable: true } instead of type unions
|
|
1170
|
+
| "openai-strict"; // OpenAI Structured Outputs strict mode
|
|
1055
1171
|
```
|
|
1056
1172
|
|
|
1057
1173
|
Pick by consumer:
|
|
1058
1174
|
|
|
1059
|
-
| Consumer
|
|
1060
|
-
|
|
|
1061
|
-
| Modern tooling, no specific reason otherwise
|
|
1062
|
-
| Swagger 2.0 / older OpenAPI / older form builders
|
|
1063
|
-
| OpenAPI 3.0 spec (uses `nullable: true`)
|
|
1175
|
+
| Consumer | Target |
|
|
1176
|
+
| --------------------------------------------------------------- | ----------------- |
|
|
1177
|
+
| Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
|
|
1178
|
+
| Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
|
|
1179
|
+
| OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
|
|
1064
1180
|
| OpenAI `response_format: { type: "json_schema", strict: true }` | `"openai-strict"` |
|
|
1065
1181
|
|
|
1066
1182
|
## OpenAI structured outputs (`openai-strict`)
|
|
@@ -1141,8 +1257,8 @@ Cleanly mapped:
|
|
|
1141
1257
|
|
|
1142
1258
|
Some seal constructs have no JSON Schema representation:
|
|
1143
1259
|
|
|
1144
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the
|
|
1145
|
-
- **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects
|
|
1260
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the _shape_, not the inter-field invariants.
|
|
1261
|
+
- **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects _post-mutator, pre-transformer_ shape (since that's what rules see and what the LLM is asked to produce for `openai-strict`).
|
|
1146
1262
|
- **`v.computed` / `v.managed`** — **skipped** entirely by the parent `v.object`; they never appear in `properties`. They aren't part of the data contract. (Calling `.toJsonSchema()` directly on one throws — the parent object is responsible for skipping them.)
|
|
1147
1263
|
- **`v.instanceof(Ctor)`** — produces `{}`. Class identity isn't expressible. For `File`, attach `{ type: "string", format: "binary" }` manually after generation if needed for OpenAPI.
|
|
1148
1264
|
- **`v.any()`** — produces `{}` deliberately (any value is valid).
|
|
@@ -1151,10 +1267,10 @@ Boolean rules `accepted` / `declined` and similar coercion-style rules are also
|
|
|
1151
1267
|
|
|
1152
1268
|
## When the generated schema rejects valid data
|
|
1153
1269
|
|
|
1154
|
-
If the schema validator itself accepts data but the
|
|
1270
|
+
If the schema validator itself accepts data but the _generated_ JSON Schema rejects the same data downstream, the cause is usually one of:
|
|
1155
1271
|
|
|
1156
1272
|
- **Cross-field rule.** The generated schema doesn't enforce it, but a separate consumer might. (Or the runtime check fired at a different stage.)
|
|
1157
|
-
- **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the
|
|
1273
|
+
- **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the _input_ still needs to be a Date-parseable thing.
|
|
1158
1274
|
- **`openai-strict` quirk.** Optional fields show as `["T", "null"]` rather than omitted — if the model omits them entirely (without sending `null`), strict mode fails. The fix is on the prompt side: tell the model to send `null` for unused fields.
|
|
1159
1275
|
|
|
1160
1276
|
## Cost note
|
|
@@ -1171,19 +1287,17 @@ description: 'Read `ValidationResult` — `isValid`, `errors[]`, `data`. Branch
|
|
|
1171
1287
|
|
|
1172
1288
|
# `ValidationResult` — reading errors
|
|
1173
1289
|
|
|
1174
|
-
`validate(schema, data)` never throws. It returns a `
|
|
1290
|
+
`validate(schema, data)` never throws. It returns a result narrowed on `isValid`:
|
|
1175
1291
|
|
|
1176
1292
|
```ts
|
|
1177
|
-
type
|
|
1178
|
-
isValid:
|
|
1179
|
-
|
|
1180
|
-
errors: ValidationError[];
|
|
1181
|
-
};
|
|
1293
|
+
type TypedValidationResult<TData> =
|
|
1294
|
+
| { isValid: true; data: TData; errors: ValidationError[] } // TData = Infer.Output<typeof schema>
|
|
1295
|
+
| { isValid: false; data: undefined; errors: ValidationError[] };
|
|
1182
1296
|
|
|
1183
1297
|
type ValidationError = {
|
|
1184
|
-
type: string;
|
|
1185
|
-
error: string;
|
|
1186
|
-
input: string;
|
|
1298
|
+
type: string; // rule type that failed — "required", "email", "min", "string", ...
|
|
1299
|
+
error: string; // resolved message (translated, with attribute substitution)
|
|
1300
|
+
input: string; // input field name — "email" or "address.city"
|
|
1187
1301
|
};
|
|
1188
1302
|
```
|
|
1189
1303
|
|
|
@@ -1202,7 +1316,7 @@ const schema = v.object({
|
|
|
1202
1316
|
const result = await validate(schema, input);
|
|
1203
1317
|
|
|
1204
1318
|
if (result.isValid) {
|
|
1205
|
-
return result.data;
|
|
1319
|
+
return result.data; // typed as the inferred shape (post-transformer)
|
|
1206
1320
|
}
|
|
1207
1321
|
|
|
1208
1322
|
return {
|
|
@@ -1217,15 +1331,13 @@ return {
|
|
|
1217
1331
|
const result = await validate(schema, input);
|
|
1218
1332
|
|
|
1219
1333
|
if (!result.isValid) {
|
|
1220
|
-
const emailMissing = result.errors.find(
|
|
1221
|
-
e => e.input === "email" && e.type === "required",
|
|
1222
|
-
);
|
|
1334
|
+
const emailMissing = result.errors.find((e) => e.input === "email" && e.type === "required");
|
|
1223
1335
|
if (emailMissing) {
|
|
1224
1336
|
return { redirect: "/signup", reason: "no email" };
|
|
1225
1337
|
}
|
|
1226
1338
|
|
|
1227
1339
|
const ageInvalid = result.errors.find(
|
|
1228
|
-
e => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
1340
|
+
(e) => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
1229
1341
|
);
|
|
1230
1342
|
if (ageInvalid) {
|
|
1231
1343
|
return { error: "Age must be 13 or older" };
|
|
@@ -1241,18 +1353,20 @@ These are the strings you'll see in `error.type`:
|
|
|
1241
1353
|
|
|
1242
1354
|
The `type` is the rule's own `name` field — not the method you called. Several methods map to one shared rule name (e.g. `.min()` / `.minLength()` on a string both surface as `minLength`; `.sameAs()` surfaces as `equalsField`). The table below lists the actual `type` strings:
|
|
1243
1355
|
|
|
1244
|
-
| Type
|
|
1245
|
-
|
|
|
1246
|
-
| `required`, `present`
|
|
1247
|
-
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless`
|
|
1248
|
-
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date`
|
|
1249
|
-
| `minLength`, `maxLength`, `betweenLength`, `length`
|
|
1250
|
-
| `min`, `max`, `betweenNumbers`
|
|
1251
|
-
| `email`, `url`, `uuid`, `pattern`, `matches`
|
|
1252
|
-
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues`
|
|
1253
|
-
| `instanceof`
|
|
1254
|
-
| `equalsField`, `notEqualsField`
|
|
1255
|
-
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`)
|
|
1356
|
+
| Type | Produced by |
|
|
1357
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
1358
|
+
| `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
|
|
1359
|
+
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
|
|
1360
|
+
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
|
|
1361
|
+
| `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
|
|
1362
|
+
| `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
|
|
1363
|
+
| `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
|
|
1364
|
+
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
|
|
1365
|
+
| `instanceof` | `v.instanceof(Ctor)` |
|
|
1366
|
+
| `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
|
|
1367
|
+
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
|
|
1368
|
+
|
|
1369
|
+
`v.union(...)` reports `union`: when no branch passes, it relays the closest branch's errors (fewest mismatched literals, then fewest errors, then deepest path) at their own paths, all typed `union`. A custom rule can do the same by returning `errors: [{ error, path }, ...]` with its failed result.
|
|
1256
1370
|
|
|
1257
1371
|
If you write custom rules, the `type` name you set on the rule object is what shows up here. Pick stable, kebab-or-camel-case names — they become a public API.
|
|
1258
1372
|
|
|
@@ -1284,7 +1398,7 @@ For attribute display names ("email" → "Email Address"), use `.attributes({ em
|
|
|
1284
1398
|
|
|
1285
1399
|
## When to throw
|
|
1286
1400
|
|
|
1287
|
-
Don't wrap `validate()` in try/catch for
|
|
1401
|
+
Don't wrap `validate()` in try/catch for _validation_ failures — those land in `result.errors`. The only thing that _throws_ is a programming bug:
|
|
1288
1402
|
|
|
1289
1403
|
- A rule's callback threw (e.g. you wrote `async validate() { throw new Error(...) }`).
|
|
1290
1404
|
- A transformer threw on output.
|
|
@@ -1300,7 +1414,7 @@ If you're surfacing seal errors through HTTP / RPC, the typical shape is:
|
|
|
1300
1414
|
if (!result.isValid) {
|
|
1301
1415
|
return reply.code(422).send({
|
|
1302
1416
|
error: "validation_failed",
|
|
1303
|
-
fields: result.errors.map(e => ({
|
|
1417
|
+
fields: result.errors.map((e) => ({
|
|
1304
1418
|
field: e.input,
|
|
1305
1419
|
type: e.type,
|
|
1306
1420
|
message: e.error,
|
|
@@ -1323,7 +1437,7 @@ description: "@warlock.js/seal overview for orientation and choosing the focused
|
|
|
1323
1437
|
|
|
1324
1438
|
# `@warlock.js/seal` — overview
|
|
1325
1439
|
|
|
1326
|
-
A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check
|
|
1440
|
+
A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check _and_ the TypeScript type _and_ (optionally) a JSON Schema.
|
|
1327
1441
|
|
|
1328
1442
|
Standalone — no framework required. Ships built-in with `@warlock.js/core`, which adds framework-specific validators (file uploads, database existence/uniqueness rules) on top.
|
|
1329
1443
|
|
|
@@ -1346,31 +1460,39 @@ Eight task skills. Most schemas only need `seal-basics` + `pick-seal-primitive`
|
|
|
1346
1460
|
### Foundations
|
|
1347
1461
|
|
|
1348
1462
|
#### `seal-basics`
|
|
1463
|
+
|
|
1349
1464
|
Start here. The `v` factory, `validate(schema, data)`, and `Infer<typeof schema>`.
|
|
1350
1465
|
|
|
1351
1466
|
### Building schemas
|
|
1352
1467
|
|
|
1353
1468
|
#### `pick-seal-primitive`
|
|
1469
|
+
|
|
1354
1470
|
Choose the right primitive — `string` / `int` / `literal` / `date` / `enum` / `computed` / `managed` / `instanceof` / `any`. Covers the close calls (`string` vs `scalar`, `int` vs `number`, `literal` vs `enum`).
|
|
1355
1471
|
|
|
1356
1472
|
#### `define-structural-shape`
|
|
1473
|
+
|
|
1357
1474
|
Compose `v.object` / `v.array` / `v.record` / `v.tuple` / `v.union` / `v.discriminatedUnion` / `v.lazy` — object schemas, dynamic-keyed records, tagged unions, recursive shapes.
|
|
1358
1475
|
|
|
1359
1476
|
#### `compose-seal-modifiers`
|
|
1477
|
+
|
|
1360
1478
|
Cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`.
|
|
1361
1479
|
|
|
1362
1480
|
### Output + interop
|
|
1363
1481
|
|
|
1364
1482
|
#### `handle-seal-errors`
|
|
1483
|
+
|
|
1365
1484
|
Read a `ValidationResult` — `isValid`, `errors[]`, `data`. Branch on `error.type`, customize messages, hook translation.
|
|
1366
1485
|
|
|
1367
1486
|
#### `generate-json-schema`
|
|
1487
|
+
|
|
1368
1488
|
`schema.toJsonSchema(target)` — draft-2020-12 / draft-07 / openapi-3.0 / openai-strict. For OpenAI structured outputs, OpenAPI specs, any JSON-Schema consumer.
|
|
1369
1489
|
|
|
1370
1490
|
#### `bridge-standard-schema`
|
|
1371
|
-
|
|
1491
|
+
|
|
1492
|
+
Standard Schema interop — why a `StandardSchemaV1<T>` slot might reject a schema, object input/output class parameters and primitive factory intersections, Cascade `Model<TSchema>` variance. For migrating off `as unknown as` casts.
|
|
1372
1493
|
|
|
1373
1494
|
#### `extend-seal-with-plugins`
|
|
1495
|
+
|
|
1374
1496
|
Author a plugin to add validator methods (`.slug`, `.postalCode`, …) — the `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation.
|
|
1375
1497
|
|
|
1376
1498
|
## Configuration
|
|
@@ -1399,25 +1521,25 @@ description: 'Pick the right `v` factory primitive — string / int / literal /
|
|
|
1399
1521
|
|
|
1400
1522
|
# Picking the right primitive
|
|
1401
1523
|
|
|
1402
|
-
This is the orientation skill —
|
|
1524
|
+
This is the orientation skill — _which_ primitive for _which_ job. For the chainable methods on each (`.email()`, `.min()`, `.between()`, etc.), load the matching `*-methods.md` reference file in this skill folder.
|
|
1403
1525
|
|
|
1404
1526
|
## Strings
|
|
1405
1527
|
|
|
1406
1528
|
```ts
|
|
1407
|
-
v.string()
|
|
1408
|
-
v.email()
|
|
1409
|
-
v.enum(["a", "b"])
|
|
1529
|
+
v.string(); // type: string — full surface in string-methods.md
|
|
1530
|
+
v.email(); // shorthand for v.string().email()
|
|
1531
|
+
v.enum(["a", "b"]); // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
|
|
1410
1532
|
```
|
|
1411
1533
|
|
|
1412
1534
|
Reach for `v.string()` for any text input. `v.email()` is just sugar — switch back to `v.string().email().min(...)` when you need extra rules.
|
|
1413
1535
|
|
|
1414
1536
|
## Numbers — pick by what you accept
|
|
1415
1537
|
|
|
1416
|
-
| Validator
|
|
1417
|
-
|
|
1418
|
-
| `v.number()`
|
|
1419
|
-
| `v.int()`
|
|
1420
|
-
| `v.float()`
|
|
1538
|
+
| Validator | Accepts | When |
|
|
1539
|
+
| ------------- | ------------------------- | ----------------------------------------------------------- |
|
|
1540
|
+
| `v.number()` | any finite number | accepts both integers and floats |
|
|
1541
|
+
| `v.int()` | integers only | rejects `1.5` |
|
|
1542
|
+
| `v.float()` | finite, non-integer | rejects `1` |
|
|
1421
1543
|
| `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
|
|
1422
1544
|
|
|
1423
1545
|
All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
|
|
@@ -1425,8 +1547,8 @@ All four share the same chain surface (see [`number-methods.md`](./number-method
|
|
|
1425
1547
|
## Booleans & scalars
|
|
1426
1548
|
|
|
1427
1549
|
```ts
|
|
1428
|
-
v.boolean()
|
|
1429
|
-
v.scalar()
|
|
1550
|
+
v.boolean(); // type: boolean — adds .accepted() / .declined() for form-style inputs
|
|
1551
|
+
v.scalar(); // type: string | number | boolean — usually a smell pointing at a missing discriminator
|
|
1430
1552
|
```
|
|
1431
1553
|
|
|
1432
1554
|
Use `v.scalar()` only when the field truly accepts any of the three primitives. If it's "one of N specific values across types", `v.literal(...)` is cleaner.
|
|
@@ -1434,8 +1556,8 @@ Use `v.scalar()` only when the field truly accepts any of the three primitives.
|
|
|
1434
1556
|
## Dates
|
|
1435
1557
|
|
|
1436
1558
|
```ts
|
|
1437
|
-
v.date()
|
|
1438
|
-
v.instanceof(Date)
|
|
1559
|
+
v.date(); // type: Date — normalizes strings/timestamps to Date, rich rule surface
|
|
1560
|
+
v.instanceof(Date); // type: Date — raw instanceof, no normalization, no rules
|
|
1439
1561
|
```
|
|
1440
1562
|
|
|
1441
1563
|
`v.date()` is the right tool 99% of the time — it ships `.min/.max/.before/.after/.weekDay/.minAge/...` and a built-in mutator that parses strings. `v.instanceof(Date)` is the escape hatch when you specifically need strict instance identity with zero coercion.
|
|
@@ -1443,14 +1565,14 @@ v.instanceof(Date) // type: Date — raw instanceof, no normalization, no
|
|
|
1443
1565
|
## Literals & instances
|
|
1444
1566
|
|
|
1445
1567
|
```ts
|
|
1446
|
-
v.literal("items")
|
|
1447
|
-
v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
|
|
1448
|
-
v.literal(1, 2, 3)
|
|
1449
|
-
v.literal(true)
|
|
1568
|
+
v.literal("items"); // type: "items"
|
|
1569
|
+
v.literal("draft", "published", "archived"); // type: "draft" | "published" | "archived"
|
|
1570
|
+
v.literal(1, 2, 3); // type: 1 | 2 | 3
|
|
1571
|
+
v.literal(true); // type: true
|
|
1450
1572
|
|
|
1451
|
-
v.instanceof(File)
|
|
1452
|
-
v.instanceof(Buffer)
|
|
1453
|
-
v.instanceof(MyClass)
|
|
1573
|
+
v.instanceof(File); // type: File
|
|
1574
|
+
v.instanceof(Buffer); // type: Buffer
|
|
1575
|
+
v.instanceof(MyClass); // type: MyClass
|
|
1454
1576
|
```
|
|
1455
1577
|
|
|
1456
1578
|
**`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
|
|
@@ -1459,20 +1581,24 @@ v.instanceof(MyClass) // type: MyClass
|
|
|
1459
1581
|
- `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
|
|
1460
1582
|
- `v.enum(["a", "b"])` runs the same `oneOf` rule at runtime (it builds a `StringValidator().oneOf(...)`), but the `v.enum` factory overload **preserves the literal union** — it infers `"a" | "b"`, not `string`. Pass a TS enum object (`v.enum(Direction)`) and it uses `Object.values`, inferring `Direction[keyof Direction]`.
|
|
1461
1583
|
|
|
1462
|
-
`**`v.literal("")
|
|
1584
|
+
`**`v.literal("")`— the empty string is a value, not an absence.**`""` is accepted when it is one of the configured literals, and the field is still required to be _present_:
|
|
1463
1585
|
|
|
1464
1586
|
```ts
|
|
1465
1587
|
const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
|
|
1466
1588
|
|
|
1467
|
-
await v.validate(decorative, { decorative: true, alt: "" });
|
|
1468
|
-
await v.validate(decorative, { decorative: true, alt: "text" });
|
|
1469
|
-
await v.validate(decorative, { decorative: true });
|
|
1589
|
+
await v.validate(decorative, { decorative: true, alt: "" }); // valid
|
|
1590
|
+
await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
|
|
1591
|
+
await v.validate(decorative, { decorative: true }); // invalid — key missing
|
|
1470
1592
|
```
|
|
1471
1593
|
|
|
1472
1594
|
That distinction matters wherever "empty" and "absent" mean different things — `alt=""` marks an image as decorative to a screen reader, while a missing `alt` is an authoring mistake. Add `.optional()` only when a missing key really is acceptable; a present value must still match the literal.
|
|
1473
1595
|
|
|
1474
1596
|
:::note[Fixed in 4.9.2]
|
|
1475
|
-
Before 4.9.2 `v.literal("")` always failed with
|
|
1597
|
+
Before 4.9.2 `v.literal("")` always failed with _"is required"_, and `.optional()` looked like a workaround while actually disabling the literal check — accepting `""`, `null`, and a missing key alike. `v.literal(0)` and `v.literal(false)` were never affected.
|
|
1598
|
+
:::
|
|
1599
|
+
|
|
1600
|
+
:::note[Fixed again in 5.30]
|
|
1601
|
+
5.25 made a blank form value count as missing for literal and enum sets, so an unselected `<select>` reads as absent. That rule also caught `v.literal("")` from 5.25 to 5.29: the required literal rejected `""` and `.optional()` dropped it from the data. Since 5.30 a literal set that lists a blank string keeps `""` as a value in its default (implicit) required state and under `.present()`, `.optional()` and `.nullable()`; a set without one still reads a blank value as missing. An explicit `.required()` (or `requiredIf` and the other conditional rules) puts back the strict non-empty rule and rejects `""` — leave the default state in place when `""` is a valid value.
|
|
1476
1602
|
:::
|
|
1477
1603
|
|
|
1478
1604
|
`v.instanceof(Ctor)` for File/Buffer/Uint8Array/custom classes. Returns `{}` from `toJsonSchema()` (not representable). For OpenAPI `File`, attach `{ type: "string", format: "binary" }` manually after generation.
|
|
@@ -1480,7 +1606,7 @@ Before 4.9.2 `v.literal("")` always failed with *"is required"*, and `.optional(
|
|
|
1480
1606
|
## `v.any` — escape hatch
|
|
1481
1607
|
|
|
1482
1608
|
```ts
|
|
1483
|
-
v.any()
|
|
1609
|
+
v.any(); // type: any — skips validation entirely
|
|
1484
1610
|
```
|
|
1485
1611
|
|
|
1486
1612
|
Reach for it when you genuinely don't care about the shape. Usually a smell — search PRs for it and ask whether a real schema would catch a class of bugs.
|
|
@@ -1502,26 +1628,26 @@ v.object({
|
|
|
1502
1628
|
- **`v.computed`** runs after sibling validation; callback signature is `(data, context)` — `data` is the validated sibling object. Use for derived values (full name, hash of fields, computed totals). An optional second arg validates the result: `v.computed<string>(cb, v.string().min(3))`.
|
|
1503
1629
|
- **`v.managed`** runs from `SchemaContext` only — callback signature is `(context)`. Caller-supplied extras passed to `validate(schema, data, { context })` land on `context.context`. Use for framework-injected values — timestamps, current user, request id. The callback is optional (`v.managed()`) for values the framework injects without a generator.
|
|
1504
1630
|
|
|
1505
|
-
Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()`
|
|
1631
|
+
Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()` _directly_ on a `v.computed` / `v.managed` validator **throws** (it's a programming error — let the parent object skip them).
|
|
1506
1632
|
|
|
1507
1633
|
## Quick map — "I need to validate…"
|
|
1508
1634
|
|
|
1509
|
-
| Need
|
|
1510
|
-
|
|
1511
|
-
| Email
|
|
1512
|
-
| URL
|
|
1513
|
-
| UUID
|
|
1514
|
-
| Number 0–100
|
|
1515
|
-
| Positive integer
|
|
1516
|
-
| One of N constants
|
|
1517
|
-
| One of TS enum values
|
|
1518
|
-
| Date in the past
|
|
1519
|
-
| File upload
|
|
1520
|
-
| Class instance (not Date)
|
|
1521
|
-
| Discriminated union
|
|
1522
|
-
| Derived value (computed from siblings) | `v.computed<T>(callback)`
|
|
1523
|
-
| Framework-injected value
|
|
1524
|
-
| Free-form / pass-through
|
|
1635
|
+
| Need | Reach for |
|
|
1636
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
1637
|
+
| Email | `v.string().email()` or `v.email()` |
|
|
1638
|
+
| URL | `v.string().url()` |
|
|
1639
|
+
| UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
|
|
1640
|
+
| Number 0–100 | `v.number().between(0, 100)` |
|
|
1641
|
+
| Positive integer | `v.int().positive()` |
|
|
1642
|
+
| One of N constants | `v.literal(...values)` |
|
|
1643
|
+
| One of TS enum values | `v.enum(MyEnum)` |
|
|
1644
|
+
| Date in the past | `v.date().past()` |
|
|
1645
|
+
| File upload | `v.instanceof(File)` |
|
|
1646
|
+
| Class instance (not Date) | `v.instanceof(Ctor)` |
|
|
1647
|
+
| Discriminated union | see the `define-structural-shape` topic |
|
|
1648
|
+
| Derived value (computed from siblings) | `v.computed<T>(callback)` |
|
|
1649
|
+
| Framework-injected value | `v.managed<T>(callback)` |
|
|
1650
|
+
| Free-form / pass-through | `v.any()` (only when you've thought about it) |
|
|
1525
1651
|
|
|
1526
1652
|
## Method-surface reference
|
|
1527
1653
|
|
|
@@ -1540,7 +1666,7 @@ For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`
|
|
|
1540
1666
|
- Don't pick `v.scalar` because "it's flexible". Flexibility at this layer usually means a missing discriminator — try `v.literal` or `v.union` first.
|
|
1541
1667
|
- Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
|
|
1542
1668
|
- Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
|
|
1543
|
-
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when
|
|
1669
|
+
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when _producing_ a value.
|
|
1544
1670
|
|
|
1545
1671
|
|
|
1546
1672
|
### pick-seal-primitive/boolean-methods.md `@warlock.js/seal/pick-seal-primitive/boolean-methods.md`
|
|
@@ -1659,139 +1785,139 @@ v.object({
|
|
|
1659
1785
|
|
|
1660
1786
|
## Range — global value comparison
|
|
1661
1787
|
|
|
1662
|
-
| Method
|
|
1663
|
-
|
|
1664
|
-
| `.min(dateOrField, msg?)`
|
|
1665
|
-
| `.max(dateOrField, msg?)`
|
|
1666
|
-
| `.before(dateOrField, msg?)` | strict `<`
|
|
1667
|
-
| `.after(dateOrField, msg?)`
|
|
1668
|
-
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)`
|
|
1788
|
+
| Method | Args | Example |
|
|
1789
|
+
| ---------------------------- | --------------- | ---------------------------------------------------------- |
|
|
1790
|
+
| `.min(dateOrField, msg?)` | inclusive `>=` | `v.date().min("2024-01-01")` or `v.date().min(new Date())` |
|
|
1791
|
+
| `.max(dateOrField, msg?)` | inclusive `<=` | `v.date().max(new Date())` |
|
|
1792
|
+
| `.before(dateOrField, msg?)` | strict `<` | `v.date().before(new Date())` |
|
|
1793
|
+
| `.after(dateOrField, msg?)` | strict `>` | `v.date().after(new Date())` |
|
|
1794
|
+
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)` |
|
|
1669
1795
|
|
|
1670
1796
|
Smart detection: a string with `-` or `/` is a date string; a plain string is a sibling field name.
|
|
1671
1797
|
|
|
1672
1798
|
## Range — explicit sibling scope
|
|
1673
1799
|
|
|
1674
|
-
| Method
|
|
1675
|
-
|
|
1676
|
-
| `.minSibling(field, msg?)`
|
|
1677
|
-
| `.maxSibling(field, msg?)`
|
|
1678
|
-
| `.beforeSibling(field, msg?)`
|
|
1679
|
-
| `.afterSibling(field, msg?)`
|
|
1680
|
-
| `.sameAsField(field, msg?)`
|
|
1800
|
+
| Method | Effect |
|
|
1801
|
+
| ---------------------------------- | ------------------------------------ |
|
|
1802
|
+
| `.minSibling(field, msg?)` | `>=` sibling field |
|
|
1803
|
+
| `.maxSibling(field, msg?)` | `<=` sibling field |
|
|
1804
|
+
| `.beforeSibling(field, msg?)` | `<` sibling field |
|
|
1805
|
+
| `.afterSibling(field, msg?)` | `>` sibling field |
|
|
1806
|
+
| `.sameAsField(field, msg?)` | `===` sibling field |
|
|
1681
1807
|
| `.sameAsFieldSibling(field, msg?)` | `===` sibling field (explicit scope) |
|
|
1682
1808
|
|
|
1683
1809
|
Only run inside `v.object`. Not representable in JSON Schema.
|
|
1684
1810
|
|
|
1685
1811
|
## Today / past / future
|
|
1686
1812
|
|
|
1687
|
-
| Method
|
|
1688
|
-
|
|
1689
|
-
| `.today(msg?)`
|
|
1690
|
-
| `.fromToday(msg?)`
|
|
1813
|
+
| Method | Effect |
|
|
1814
|
+
| -------------------- | --------------------- |
|
|
1815
|
+
| `.today(msg?)` | exactly today |
|
|
1816
|
+
| `.fromToday(msg?)` | today or future |
|
|
1691
1817
|
| `.beforeToday(msg?)` | strictly before today |
|
|
1692
|
-
| `.afterToday(msg?)`
|
|
1693
|
-
| `.past(msg?)`
|
|
1694
|
-
| `.future(msg?)`
|
|
1818
|
+
| `.afterToday(msg?)` | strictly after today |
|
|
1819
|
+
| `.past(msg?)` | any past date |
|
|
1820
|
+
| `.future(msg?)` | any future date |
|
|
1695
1821
|
|
|
1696
1822
|
## Relative window
|
|
1697
1823
|
|
|
1698
|
-
| Method
|
|
1699
|
-
|
|
1700
|
-
| `.withinDays(n, msg?)`
|
|
1701
|
-
| `.withinPastDays(n, msg?)`
|
|
1702
|
-
| `.withinFutureDays(n, msg?)` | within N days in the future
|
|
1824
|
+
| Method | Args | Effect |
|
|
1825
|
+
| ---------------------------- | ---------------------------- | ------ |
|
|
1826
|
+
| `.withinDays(n, msg?)` | within N days past or future | — |
|
|
1827
|
+
| `.withinPastDays(n, msg?)` | within N days in the past | — |
|
|
1828
|
+
| `.withinFutureDays(n, msg?)` | within N days in the future | — |
|
|
1703
1829
|
|
|
1704
1830
|
## Age
|
|
1705
1831
|
|
|
1706
|
-
| Method
|
|
1707
|
-
|
|
1708
|
-
| `.age(years, msg?)`
|
|
1709
|
-
| `.minAge(years, msg?)`
|
|
1710
|
-
| `.maxAge(years, msg?)`
|
|
1711
|
-
| `.betweenAge(min, max, msg?)`
|
|
1832
|
+
| Method | Args | Effect |
|
|
1833
|
+
| ----------------------------------- | --------------------------------- | ------ |
|
|
1834
|
+
| `.age(years, msg?)` | exactly N years old |
|
|
1835
|
+
| `.minAge(years, msg?)` | at least N years old |
|
|
1836
|
+
| `.maxAge(years, msg?)` | at most N years old |
|
|
1837
|
+
| `.betweenAge(min, max, msg?)` | between min/max years |
|
|
1712
1838
|
| `.birthday(minAge?, maxAge?, msg?)` | not in future, optional age range |
|
|
1713
1839
|
|
|
1714
1840
|
## Weekday / weekend / business day
|
|
1715
1841
|
|
|
1716
|
-
| Method
|
|
1717
|
-
|
|
1718
|
-
| `.weekDay(day, msg?)`
|
|
1719
|
-
| `.weekdays(days, msg?)` | array of weekdays
|
|
1720
|
-
| `.weekend(msg?)`
|
|
1721
|
-
| `.businessDay(msg?)`
|
|
1842
|
+
| Method | Args | Effect |
|
|
1843
|
+
| ----------------------- | ----------------------------- | ------ |
|
|
1844
|
+
| `.weekDay(day, msg?)` | day = `"monday"` … `"sunday"` |
|
|
1845
|
+
| `.weekdays(days, msg?)` | array of weekdays |
|
|
1846
|
+
| `.weekend(msg?)` | Saturday or Sunday |
|
|
1847
|
+
| `.businessDay(msg?)` | Monday – Friday |
|
|
1722
1848
|
|
|
1723
1849
|
## Period — month / year / quarter
|
|
1724
1850
|
|
|
1725
|
-
| Method
|
|
1726
|
-
|
|
1727
|
-
| `.month(m, msg?)`
|
|
1728
|
-
| `.year(y, msg?)`
|
|
1729
|
-
| `.quarter(q, msg?)`
|
|
1730
|
-
| `.leapYear(msg?)`
|
|
1731
|
-
| `.minYear(yearOrField, msg?)`
|
|
1732
|
-
| `.maxYear(yearOrField, msg?)`
|
|
1733
|
-
| `.minMonth(mOrField, msg?)`
|
|
1734
|
-
| `.maxMonth(mOrField, msg?)`
|
|
1735
|
-
| `.minDay(dOrField, msg?)`
|
|
1736
|
-
| `.maxDay(dOrField, msg?)`
|
|
1737
|
-
| `.betweenYears(start, end, msg?)`
|
|
1738
|
-
| `.betweenMonths(start, end, msg?)` | inclusive month range
|
|
1739
|
-
| `.betweenDays(start, end, msg?)`
|
|
1851
|
+
| Method | Args | Effect |
|
|
1852
|
+
| ---------------------------------- | ---------------------------- | ------ |
|
|
1853
|
+
| `.month(m, msg?)` | `m` = 1–12 (or `Month` enum) |
|
|
1854
|
+
| `.year(y, msg?)` | exact year |
|
|
1855
|
+
| `.quarter(q, msg?)` | `q` = 1–4 |
|
|
1856
|
+
| `.leapYear(msg?)` | year is a leap year |
|
|
1857
|
+
| `.minYear(yearOrField, msg?)` | year `>=` |
|
|
1858
|
+
| `.maxYear(yearOrField, msg?)` | year `<=` |
|
|
1859
|
+
| `.minMonth(mOrField, msg?)` | month `>=` |
|
|
1860
|
+
| `.maxMonth(mOrField, msg?)` | month `<=` |
|
|
1861
|
+
| `.minDay(dOrField, msg?)` | day-of-month `>=` |
|
|
1862
|
+
| `.maxDay(dOrField, msg?)` | day-of-month `<=` |
|
|
1863
|
+
| `.betweenYears(start, end, msg?)` | inclusive year range |
|
|
1864
|
+
| `.betweenMonths(start, end, msg?)` | inclusive month range |
|
|
1865
|
+
| `.betweenDays(start, end, msg?)` | inclusive day-of-month range |
|
|
1740
1866
|
|
|
1741
1867
|
Each `min*` / `max*` / `between*` accepts a sibling field name. Sibling-explicit variants exist: `.minYearSibling`, `.maxYearSibling`, `.minMonthSibling`, `.maxMonthSibling`, `.minDaySibling`, `.maxDaySibling`, `.betweenYearsSibling`, `.betweenMonthsSibling`, `.betweenDaysSibling`.
|
|
1742
1868
|
|
|
1743
1869
|
## Time — hour / minute
|
|
1744
1870
|
|
|
1745
|
-
| Method
|
|
1746
|
-
|
|
1747
|
-
| `.fromHour(h, msg?)`
|
|
1748
|
-
| `.beforeHour(h, msg?)`
|
|
1749
|
-
| `.betweenHours(start, end, msg?)`
|
|
1750
|
-
| `.fromMinute(m, msg?)`
|
|
1751
|
-
| `.beforeMinute(m, msg?)`
|
|
1752
|
-
| `.betweenMinutes(start, end, msg?)` | inclusive minute range
|
|
1753
|
-
| `.betweenTimes(start, end, msg?)`
|
|
1871
|
+
| Method | Args | Effect |
|
|
1872
|
+
| ----------------------------------- | ------------------------ | ------ |
|
|
1873
|
+
| `.fromHour(h, msg?)` | h = 0–23, time `>= h:00` |
|
|
1874
|
+
| `.beforeHour(h, msg?)` | time `< h:00` |
|
|
1875
|
+
| `.betweenHours(start, end, msg?)` | inclusive hour range |
|
|
1876
|
+
| `.fromMinute(m, msg?)` | m = 0–59 |
|
|
1877
|
+
| `.beforeMinute(m, msg?)` | — |
|
|
1878
|
+
| `.betweenMinutes(start, end, msg?)` | inclusive minute range |
|
|
1879
|
+
| `.betweenTimes(start, end, msg?)` | "HH:MM" strings |
|
|
1754
1880
|
|
|
1755
1881
|
## Format
|
|
1756
1882
|
|
|
1757
|
-
| Method
|
|
1758
|
-
|
|
1883
|
+
| Method | Args | Effect |
|
|
1884
|
+
| -------------------- | ------------------- | --------------------------- |
|
|
1759
1885
|
| `.format(fmt, msg?)` | dayjs format string | input must match the format |
|
|
1760
1886
|
|
|
1761
1887
|
## Mutators (pre-validation reshape)
|
|
1762
1888
|
|
|
1763
|
-
| Method
|
|
1764
|
-
|
|
1765
|
-
| `.toStartOfDay()`
|
|
1766
|
-
| `.toEndOfDay()`
|
|
1767
|
-
| `.toStartOfMonth()` | first day of month
|
|
1768
|
-
| `.toEndOfMonth()`
|
|
1769
|
-
| `.toStartOfYear()`
|
|
1770
|
-
| `.toEndOfYear()`
|
|
1771
|
-
| `.addDays(n)`
|
|
1772
|
-
| `.addMonths(n)`
|
|
1773
|
-
| `.addYears(n)`
|
|
1774
|
-
| `.addHours(n)`
|
|
1775
|
-
| `.toUTC()`
|
|
1776
|
-
|
|
1777
|
-
Mutators run
|
|
1889
|
+
| Method | Effect |
|
|
1890
|
+
| ------------------- | --------------------------------- |
|
|
1891
|
+
| `.toStartOfDay()` | 00:00:00.000 |
|
|
1892
|
+
| `.toEndOfDay()` | 23:59:59.999 |
|
|
1893
|
+
| `.toStartOfMonth()` | first day of month |
|
|
1894
|
+
| `.toEndOfMonth()` | last day of month |
|
|
1895
|
+
| `.toStartOfYear()` | January 1st |
|
|
1896
|
+
| `.toEndOfYear()` | December 31st |
|
|
1897
|
+
| `.addDays(n)` | shift by N days (negative = back) |
|
|
1898
|
+
| `.addMonths(n)` | shift by N months |
|
|
1899
|
+
| `.addYears(n)` | shift by N years |
|
|
1900
|
+
| `.addHours(n)` | shift by N hours |
|
|
1901
|
+
| `.toUTC()` | normalize to UTC |
|
|
1902
|
+
|
|
1903
|
+
Mutators run _before_ rules. `v.date().addDays(7).future()` checks whether the shifted date is in the future.
|
|
1778
1904
|
|
|
1779
1905
|
## Transformers (post-validation, reshape `data`)
|
|
1780
1906
|
|
|
1781
|
-
| Method
|
|
1782
|
-
|
|
1783
|
-
| `.toISOString()` | —
|
|
1784
|
-
| `.toTimestamp()` | —
|
|
1785
|
-
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string
|
|
1786
|
-
| `.toDateOnly()`
|
|
1787
|
-
| `.toTimeOnly()`
|
|
1907
|
+
| Method | Args | Effect |
|
|
1908
|
+
| ---------------- | ------------------- | ------------------------------------- |
|
|
1909
|
+
| `.toISOString()` | — | `Date` → `"2026-01-15T00:00:00.000Z"` |
|
|
1910
|
+
| `.toTimestamp()` | — | `Date` → number (ms since epoch) |
|
|
1911
|
+
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string |
|
|
1912
|
+
| `.toDateOnly()` | — | `"YYYY-MM-DD"` |
|
|
1913
|
+
| `.toTimeOnly()` | — | `"HH:mm:ss"` |
|
|
1788
1914
|
|
|
1789
|
-
Transformers shape the
|
|
1915
|
+
Transformers shape the _output_. `Infer<>` still resolves to `Date` even if a transformer changes the runtime shape — `Infer` reads the validator type, not the transformer pipeline.
|
|
1790
1916
|
|
|
1791
1917
|
## Defaults
|
|
1792
1918
|
|
|
1793
|
-
| Method
|
|
1794
|
-
|
|
1919
|
+
| Method | Effect |
|
|
1920
|
+
| --------------- | ------------------------------------------ |
|
|
1795
1921
|
| `.defaultNow()` | shorthand for `.default(() => new Date())` |
|
|
1796
1922
|
|
|
1797
1923
|
## JSON Schema mapping
|
|
@@ -1805,28 +1931,28 @@ Transformers shape the *output*. `Infer<>` still resolves to `Date` even if a tr
|
|
|
1805
1931
|
|
|
1806
1932
|
```ts
|
|
1807
1933
|
// Birthday — must be 13+ and in the past
|
|
1808
|
-
v.date().past().minAge(13)
|
|
1934
|
+
v.date().past().minAge(13);
|
|
1809
1935
|
|
|
1810
1936
|
// Reservation — future, business day, between hours
|
|
1811
|
-
v.date().future().businessDay().betweenHours(9, 17)
|
|
1937
|
+
v.date().future().businessDay().betweenHours(9, 17);
|
|
1812
1938
|
|
|
1813
1939
|
// Effective date range (cross-field)
|
|
1814
1940
|
v.object({
|
|
1815
1941
|
startsAt: v.date(),
|
|
1816
1942
|
endsAt: v.date().afterSibling("startsAt"),
|
|
1817
|
-
})
|
|
1943
|
+
});
|
|
1818
1944
|
|
|
1819
1945
|
// API response — emit ISO string
|
|
1820
|
-
v.date().toISOString()
|
|
1946
|
+
v.date().toISOString();
|
|
1821
1947
|
|
|
1822
1948
|
// Event window — within 30 future days, normalized to UTC
|
|
1823
|
-
v.date().toUTC().withinFutureDays(30)
|
|
1949
|
+
v.date().toUTC().withinFutureDays(30);
|
|
1824
1950
|
|
|
1825
1951
|
// Quarterly report
|
|
1826
|
-
v.date().quarter(1).year(2026)
|
|
1952
|
+
v.date().quarter(1).year(2026);
|
|
1827
1953
|
|
|
1828
1954
|
// Date-only, end-of-day for inclusive comparisons
|
|
1829
|
-
v.date().toEndOfDay().toDateOnly()
|
|
1955
|
+
v.date().toEndOfDay().toDateOnly();
|
|
1830
1956
|
```
|
|
1831
1957
|
|
|
1832
1958
|
|
|
@@ -1838,23 +1964,23 @@ All four share this surface. The picking guide (which factory to call) is in the
|
|
|
1838
1964
|
|
|
1839
1965
|
## Range — global value comparison
|
|
1840
1966
|
|
|
1841
|
-
| Method
|
|
1842
|
-
|
|
1843
|
-
| `.min(n, msg?)`
|
|
1844
|
-
| `.max(n, msg?)`
|
|
1845
|
-
| `.between(a, b, msg?)`
|
|
1846
|
-
| `.greaterThan(n, msg?)` | strict `>`
|
|
1847
|
-
| `.gt(n, msg?)`
|
|
1848
|
-
| `.lessThan(n, msg?)`
|
|
1849
|
-
| `.lt(n, msg?)`
|
|
1967
|
+
| Method | Args | JSON Schema | Example |
|
|
1968
|
+
| ----------------------- | ------------------------ | ------------------------ | -------------------------- |
|
|
1969
|
+
| `.min(n, msg?)` | inclusive lower bound | `minimum: n` | `v.int().min(0)` |
|
|
1970
|
+
| `.max(n, msg?)` | inclusive upper bound | `maximum: n` | `v.int().max(100)` |
|
|
1971
|
+
| `.between(a, b, msg?)` | inclusive range | `minimum: a, maximum: b` | `v.number().between(0, 1)` |
|
|
1972
|
+
| `.greaterThan(n, msg?)` | strict `>` | `exclusiveMinimum: n` | `v.int().greaterThan(0)` |
|
|
1973
|
+
| `.gt(n, msg?)` | alias for `.greaterThan` | `exclusiveMinimum: n` | — |
|
|
1974
|
+
| `.lessThan(n, msg?)` | strict `<` | `exclusiveMaximum: n` | `v.int().lessThan(100)` |
|
|
1975
|
+
| `.lt(n, msg?)` | alias for `.lessThan` | `exclusiveMaximum: n` | — |
|
|
1850
1976
|
|
|
1851
1977
|
`.min` / `.max` / `.between` / `.greaterThan` / `.lessThan` accept a **string** as the value — interpreted as a sibling field name (smart detection):
|
|
1852
1978
|
|
|
1853
1979
|
```ts
|
|
1854
1980
|
v.object({
|
|
1855
1981
|
minPrice: v.int(),
|
|
1856
|
-
maxPrice: v.int().min("minPrice"),
|
|
1857
|
-
})
|
|
1982
|
+
maxPrice: v.int().min("minPrice"), // maxPrice >= minPrice
|
|
1983
|
+
});
|
|
1858
1984
|
```
|
|
1859
1985
|
|
|
1860
1986
|
Sibling references are **not representable in JSON Schema** — silently omitted from generated output.
|
|
@@ -1863,68 +1989,68 @@ Sibling references are **not representable in JSON Schema** — silently omitted
|
|
|
1863
1989
|
|
|
1864
1990
|
For when smart detection is ambiguous (e.g. a numeric string that happens to match a field name):
|
|
1865
1991
|
|
|
1866
|
-
| Method
|
|
1867
|
-
|
|
1868
|
-
| `.minSibling(field, msg?)`
|
|
1869
|
-
| `.maxSibling(field, msg?)`
|
|
1870
|
-
| `.greaterThanSibling(field, msg?)`
|
|
1871
|
-
| `.gtSibling(field, msg?)`
|
|
1872
|
-
| `.lessThanSibling(field, msg?)`
|
|
1873
|
-
| `.ltSibling(field, msg?)`
|
|
1992
|
+
| Method | Effect |
|
|
1993
|
+
| ------------------------------------------- | -------------------------- |
|
|
1994
|
+
| `.minSibling(field, msg?)` | value `>=` sibling field |
|
|
1995
|
+
| `.maxSibling(field, msg?)` | value `<=` sibling field |
|
|
1996
|
+
| `.greaterThanSibling(field, msg?)` | value `>` sibling field |
|
|
1997
|
+
| `.gtSibling(field, msg?)` | alias |
|
|
1998
|
+
| `.lessThanSibling(field, msg?)` | value `<` sibling field |
|
|
1999
|
+
| `.ltSibling(field, msg?)` | alias |
|
|
1874
2000
|
| `.betweenSibling(minField, maxField, msg?)` | between two sibling fields |
|
|
1875
2001
|
|
|
1876
2002
|
These **only run inside `v.object`** — sibling resolution silently passes otherwise.
|
|
1877
2003
|
|
|
1878
2004
|
## Sign & parity
|
|
1879
2005
|
|
|
1880
|
-
| Method
|
|
1881
|
-
|
|
1882
|
-
| `.positive(msg?)` | value `> 0`
|
|
1883
|
-
| `.negative(msg?)` | value `< 0`
|
|
1884
|
-
| `.odd(msg?)`
|
|
1885
|
-
| `.even(msg?)`
|
|
2006
|
+
| Method | Effect | Example |
|
|
2007
|
+
| ----------------- | ------------- | -------------------- |
|
|
2008
|
+
| `.positive(msg?)` | value `> 0` | `v.int().positive()` |
|
|
2009
|
+
| `.negative(msg?)` | value `< 0` | `v.int().negative()` |
|
|
2010
|
+
| `.odd(msg?)` | value is odd | `v.int().odd()` |
|
|
2011
|
+
| `.even(msg?)` | value is even | `v.int().even()` |
|
|
1886
2012
|
|
|
1887
2013
|
## Divisibility / modulo
|
|
1888
2014
|
|
|
1889
|
-
| Method
|
|
1890
|
-
|
|
1891
|
-
| `.modulo(n, msg?)`
|
|
1892
|
-
| `.divisibleBy(n, msg?)` | alias
|
|
1893
|
-
| `.multipleOf(n, msg?)`
|
|
1894
|
-
| `.modulusOf(n, msg?)`
|
|
2015
|
+
| Method | Effect | JSON Schema |
|
|
2016
|
+
| ----------------------- | ----------------- | --------------- |
|
|
2017
|
+
| `.modulo(n, msg?)` | value `% n === 0` | `multipleOf: n` |
|
|
2018
|
+
| `.divisibleBy(n, msg?)` | alias | `multipleOf: n` |
|
|
2019
|
+
| `.multipleOf(n, msg?)` | alias | `multipleOf: n` |
|
|
2020
|
+
| `.modulusOf(n, msg?)` | alias | `multipleOf: n` |
|
|
1895
2021
|
|
|
1896
2022
|
## String-form length (rare)
|
|
1897
2023
|
|
|
1898
2024
|
When the numeric value is a fixed-format code (PINs, IDs):
|
|
1899
2025
|
|
|
1900
|
-
| Method
|
|
1901
|
-
|
|
1902
|
-
| `.length(n, msg?)`
|
|
1903
|
-
| `.minLength(n, msg?)` | string-rep length `>= n`
|
|
1904
|
-
| `.maxLength(n, msg?)` | string-rep length `<= n`
|
|
2026
|
+
| Method | Effect |
|
|
2027
|
+
| --------------------- | ------------------------------------- |
|
|
2028
|
+
| `.length(n, msg?)` | string-rep length must be exactly `n` |
|
|
2029
|
+
| `.minLength(n, msg?)` | string-rep length `>= n` |
|
|
2030
|
+
| `.maxLength(n, msg?)` | string-rep length `<= n` |
|
|
1905
2031
|
|
|
1906
2032
|
For most numeric-shaped IDs you'd use `v.string().length(n).numeric()` instead — let the value be a string.
|
|
1907
2033
|
|
|
1908
2034
|
## Mutators (pre-validation reshape)
|
|
1909
2035
|
|
|
1910
|
-
| Method
|
|
1911
|
-
|
|
1912
|
-
| `.abs()`
|
|
1913
|
-
| `.ceil()`
|
|
1914
|
-
| `.floor()`
|
|
1915
|
-
| `.round(decimals?)`
|
|
1916
|
-
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number**
|
|
1917
|
-
| `.coerce()`
|
|
2036
|
+
| Method | Args | Effect |
|
|
2037
|
+
| --------------------- | --------- | -------------------------------------------------------------------------------- |
|
|
2038
|
+
| `.abs()` | — | `Math.abs(value)` |
|
|
2039
|
+
| `.ceil()` | — | round up to integer |
|
|
2040
|
+
| `.floor()` | — | round down to integer |
|
|
2041
|
+
| `.round(decimals?)` | default 0 | round to N decimals |
|
|
2042
|
+
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number** |
|
|
2043
|
+
| `.coerce()` | — | a **numeric-shaped string** → `Number`; any other value passes through unchanged |
|
|
1918
2044
|
|
|
1919
|
-
These run
|
|
2045
|
+
These run _before_ validation rules. If you mutate `1.6` with `.ceil()`, `v.int()` sees `2` and passes. Use mutators when the input arrives in a slightly wrong form and you want to coerce, not reject.
|
|
1920
2046
|
|
|
1921
2047
|
`.coerce()` (5.9+) is the opt-in for the most common case: **query-string params arrive as strings.** `v.int()` is strict by default — `?limit=2` is the string `"2"` and is rejected — so add `.coerce()` when a schema validates `request.query`:
|
|
1922
2048
|
|
|
1923
2049
|
```ts
|
|
1924
|
-
v.object({ limit: v.int().coerce().min(1).max(100) })
|
|
2050
|
+
v.object({ limit: v.int().coerce().min(1).max(100) }); // "2" → 2, passes
|
|
1925
2051
|
```
|
|
1926
2052
|
|
|
1927
|
-
It only fixes the string
|
|
2053
|
+
It only fixes the string _representation_ — the type rule still runs, so `v.int().coerce()` on `"5.5"` coerces to `5.5` and correctly **fails** (still not an integer), and `"abc"` passes through and fails. Strictness is unchanged unless you chain `.coerce()`; the output type is unaffected (`v.int().coerce()` still infers `number`). Prefer this over hand-rolling `.addMutator(...)` in app schemas.
|
|
1928
2054
|
|
|
1929
2055
|
That ordering is exactly why `.toFixed()` yields a number rather than a fixed-point string: the mutated value is what the validator's own type rule sees, and a `number` schema that emitted `"3.14"` would reject its own output. Format to a string at the presentation edge instead.
|
|
1930
2056
|
|
|
@@ -1944,31 +2070,31 @@ Before 4.9.2 `.toFixed()` returned a string, so `v.number().toFixed(2)` **always
|
|
|
1944
2070
|
|
|
1945
2071
|
```ts
|
|
1946
2072
|
// Age
|
|
1947
|
-
v.int().min(0).max(150)
|
|
2073
|
+
v.int().min(0).max(150);
|
|
1948
2074
|
|
|
1949
2075
|
// Price (cents)
|
|
1950
|
-
v.int().min(0)
|
|
2076
|
+
v.int().min(0);
|
|
1951
2077
|
|
|
1952
2078
|
// Probability
|
|
1953
|
-
v.number().between(0, 1)
|
|
2079
|
+
v.number().between(0, 1);
|
|
1954
2080
|
|
|
1955
2081
|
// Even page index
|
|
1956
|
-
v.int().min(0).even()
|
|
2082
|
+
v.int().min(0).even();
|
|
1957
2083
|
|
|
1958
2084
|
// Quantity divisible by box size
|
|
1959
|
-
v.int().min(1).multipleOf(12)
|
|
2085
|
+
v.int().min(1).multipleOf(12);
|
|
1960
2086
|
|
|
1961
2087
|
// Coerced from form input
|
|
1962
|
-
v.numeric().min(0).max(100)
|
|
2088
|
+
v.numeric().min(0).max(100);
|
|
1963
2089
|
|
|
1964
2090
|
// Cross-field range
|
|
1965
2091
|
v.object({
|
|
1966
2092
|
startYear: v.int(),
|
|
1967
2093
|
endYear: v.int().minSibling("startYear"),
|
|
1968
|
-
})
|
|
2094
|
+
});
|
|
1969
2095
|
|
|
1970
2096
|
// Optional with default
|
|
1971
|
-
v.int().min(0).default(0).optional()
|
|
2097
|
+
v.int().min(0).default(0).optional();
|
|
1972
2098
|
```
|
|
1973
2099
|
|
|
1974
2100
|
|
|
@@ -1982,168 +2108,173 @@ For the picking guide (`v.string` vs `v.scalar` vs `v.literal`), see the `pick-s
|
|
|
1982
2108
|
|
|
1983
2109
|
## Length
|
|
1984
2110
|
|
|
1985
|
-
| Method
|
|
1986
|
-
|
|
1987
|
-
| `.min(n, msg?)`
|
|
1988
|
-
| `.minLength(n, msg?)`
|
|
1989
|
-
| `.max(n, msg?)`
|
|
1990
|
-
| `.maxLength(n, msg?)`
|
|
1991
|
-
| `.length(n, msg?)`
|
|
1992
|
-
| `.lengthBetween(a, b, msg?)` | min and max length
|
|
2111
|
+
| Method | Args | JSON Schema | Example |
|
|
2112
|
+
| ---------------------------- | --------------------- | ---------------------------- | --------------------------------- |
|
|
2113
|
+
| `.min(n, msg?)` | min length, inclusive | `minLength: n` | `v.string().min(3)` |
|
|
2114
|
+
| `.minLength(n, msg?)` | alias for `.min()` | `minLength: n` | `v.string().minLength(3)` |
|
|
2115
|
+
| `.max(n, msg?)` | max length, inclusive | `maxLength: n` | `v.string().max(120)` |
|
|
2116
|
+
| `.maxLength(n, msg?)` | alias for `.max()` | `maxLength: n` | `v.string().maxLength(120)` |
|
|
2117
|
+
| `.length(n, msg?)` | exact length | `minLength=maxLength=n` | `v.string().length(10)` |
|
|
2118
|
+
| `.lengthBetween(a, b, msg?)` | min and max length | `minLength: a, maxLength: b` | `v.string().lengthBetween(5, 30)` |
|
|
1993
2119
|
|
|
1994
2120
|
## Format
|
|
1995
2121
|
|
|
1996
|
-
| Method
|
|
1997
|
-
|
|
1998
|
-
| `.email(msg?)`
|
|
1999
|
-
| `.url(msg?)`
|
|
2000
|
-
| `.pattern(re, msg?)`
|
|
2001
|
-
| `.alpha(msg?)`
|
|
2002
|
-
| `.alphanumeric(msg?)`
|
|
2003
|
-
| `.numeric(msg?)`
|
|
2004
|
-
| `.withoutWhitespace(msg?)`
|
|
2005
|
-
| `.creditCard(msg?)`
|
|
2006
|
-
| `.ip(msg?)`
|
|
2007
|
-
| `.ip4(msg?)`
|
|
2008
|
-
| `.ip6(msg?)`
|
|
2009
|
-
| `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | —
|
|
2122
|
+
| Method | Pattern | JSON Schema | Example |
|
|
2123
|
+
| -------------------------------- | ---------------------------------- | -------------------- | -------------------------------- |
|
|
2124
|
+
| `.email(msg?)` | RFC-flavored regex | `format: "email"` | `v.string().email()` |
|
|
2125
|
+
| `.url(msg?)` | http/https URL | `format: "uri"` | `v.string().url()` |
|
|
2126
|
+
| `.pattern(re, msg?)` | custom regex | `pattern: re.source` | `v.string().pattern(/^[A-Z]/)` |
|
|
2127
|
+
| `.alpha(msg?)` | letters only | — | `v.string().alpha()` |
|
|
2128
|
+
| `.alphanumeric(msg?)` | letters + digits | — | `v.string().alphanumeric()` |
|
|
2129
|
+
| `.numeric(msg?)` | digits only (string) | — | `v.string().numeric()` |
|
|
2130
|
+
| `.withoutWhitespace(msg?)` | no spaces/tabs/newlines | — | `v.string().withoutWhitespace()` |
|
|
2131
|
+
| `.creditCard(msg?)` | credit-card-shaped | — | `v.string().creditCard()` |
|
|
2132
|
+
| `.ip(msg?)` | IPv4 or IPv6 | `format: "ipv4"` | `v.string().ip()` |
|
|
2133
|
+
| `.ip4(msg?)` | IPv4 only | `format: "ipv4"` | `v.string().ip4()` |
|
|
2134
|
+
| `.ip6(msg?)` | IPv6 only | `format: "ipv6"` | `v.string().ip6()` |
|
|
2135
|
+
| `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | — | `v.string().strongPassword(12)` |
|
|
2010
2136
|
|
|
2011
2137
|
## ID formats
|
|
2012
2138
|
|
|
2013
|
-
| Method
|
|
2014
|
-
|
|
2015
|
-
| `.uuid(version?, msg?)`
|
|
2016
|
-
| `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …`
|
|
2017
|
-
| `.ulid(msg?)`
|
|
2018
|
-
| `.nanoid(length?, msg?)`
|
|
2139
|
+
| Method | Args | JSON Schema | Example |
|
|
2140
|
+
| ---------------------------- | ---------------------------------------------- | ---------------- | ----------------------- |
|
|
2141
|
+
| `.uuid(version?, msg?)` | UUID, any version or restrict to 1/3/4/5/6/7 | `format: "uuid"` | `v.string().uuid(4)` |
|
|
2142
|
+
| `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …` | `v.string().cuid()` |
|
|
2143
|
+
| `.ulid(msg?)` | 26 chars, Crockford base32 (no I/L/O/U) | `pattern: …` | `v.string().ulid()` |
|
|
2144
|
+
| `.nanoid(length?, msg?)` | URL-safe alphabet, default length 21 | `pattern: …` | `v.string().nanoid(21)` |
|
|
2019
2145
|
|
|
2020
2146
|
UUID validation is RFC 4122 strict — the variant nibble (8/9/a/b at position 17) is checked, so "looks-like-UUID-but-not-valid" inputs are rejected. CUID defaults to **CUID2** since CUID1 is deprecated by its original author; pass `{ version: 1 }` only for legacy data. nanoid's alphabet is fixed (`A-Za-z0-9_-`) — for custom alphabets use `.pattern()` directly.
|
|
2021
2147
|
|
|
2022
2148
|
## Word count
|
|
2023
2149
|
|
|
2024
|
-
| Method
|
|
2025
|
-
|
|
2026
|
-
| `.words(n, msg?)`
|
|
2027
|
-
| `.minWords(n, msg?)` | min words
|
|
2028
|
-
| `.maxWords(n, msg?)` | max words
|
|
2150
|
+
| Method | Args | Example |
|
|
2151
|
+
| -------------------- | ---------------- | ------------------------- |
|
|
2152
|
+
| `.words(n, msg?)` | exact word count | `v.string().words(5)` |
|
|
2153
|
+
| `.minWords(n, msg?)` | min words | `v.string().minWords(3)` |
|
|
2154
|
+
| `.maxWords(n, msg?)` | max words | `v.string().maxWords(50)` |
|
|
2029
2155
|
|
|
2030
2156
|
## Content
|
|
2031
2157
|
|
|
2032
|
-
| Method
|
|
2033
|
-
|
|
2034
|
-
| `.startsWith(s, msg?)`
|
|
2035
|
-
| `.endsWith(s, msg?)`
|
|
2036
|
-
| `.contains(s, msg?)`
|
|
2158
|
+
| Method | Args | Example |
|
|
2159
|
+
| ----------------------- | ----------------- | --------------------------------------- |
|
|
2160
|
+
| `.startsWith(s, msg?)` | prefix check | `v.string().startsWith("https://")` |
|
|
2161
|
+
| `.endsWith(s, msg?)` | suffix check | `v.string().endsWith(".pdf")` |
|
|
2162
|
+
| `.contains(s, msg?)` | substring check | `v.string().contains("@")` |
|
|
2037
2163
|
| `.notContains(s, msg?)` | inverse substring | `v.string().notContains("javascript:")` |
|
|
2038
2164
|
|
|
2039
2165
|
## Color
|
|
2040
2166
|
|
|
2041
|
-
| Method
|
|
2042
|
-
|
|
2043
|
-
| `.color(msg?)`
|
|
2044
|
-
| `.hexColor(msg?)`
|
|
2045
|
-
| `.rgbColor(msg?)`
|
|
2046
|
-
| `.rgbaColor(msg?)`
|
|
2047
|
-
| `.hslColor(msg?)`
|
|
2048
|
-
| `.lightColor(msg?)` | luminance-based
|
|
2049
|
-
| `.darkColor(msg?)`
|
|
2167
|
+
| Method | Accepts | Example |
|
|
2168
|
+
| ------------------- | ------------------- | ----------------------- |
|
|
2169
|
+
| `.color(msg?)` | any valid CSS color | `v.string().color()` |
|
|
2170
|
+
| `.hexColor(msg?)` | `#rgb`, `#rrggbb` | `v.string().hexColor()` |
|
|
2171
|
+
| `.rgbColor(msg?)` | `rgb(r,g,b)` | — |
|
|
2172
|
+
| `.rgbaColor(msg?)` | `rgba(r,g,b,a)` | — |
|
|
2173
|
+
| `.hslColor(msg?)` | `hsl(h,s,l)` | — |
|
|
2174
|
+
| `.lightColor(msg?)` | luminance-based | — |
|
|
2175
|
+
| `.darkColor(msg?)` | luminance-based | — |
|
|
2050
2176
|
|
|
2051
2177
|
JSON Schema: `format: "color"` for `.hexColor()`; the others map to `format: "color"` only via `.hexColor()` — for OpenAPI consumers expecting strict format, prefer `.hexColor()`.
|
|
2052
2178
|
|
|
2053
2179
|
## Case mutators (pre-validation)
|
|
2054
2180
|
|
|
2055
|
-
| Method
|
|
2056
|
-
|
|
2057
|
-
| `.uppercase()`
|
|
2058
|
-
| `.lowercase()`
|
|
2059
|
-
| `.capitalize()` | "hello world" → "Hello world" | —
|
|
2060
|
-
| `.titleCase()`
|
|
2061
|
-
| `.camelCase()`
|
|
2062
|
-
| `.pascalCase()` | "hello world" → "HelloWorld"
|
|
2063
|
-
| `.snakeCase()`
|
|
2064
|
-
| `.kebabCase()`
|
|
2181
|
+
| Method | Effect | Example |
|
|
2182
|
+
| --------------- | ----------------------------- | ------------------------ |
|
|
2183
|
+
| `.uppercase()` | "Hello" → "HELLO" | `v.string().uppercase()` |
|
|
2184
|
+
| `.lowercase()` | "Hello" → "hello" | — |
|
|
2185
|
+
| `.capitalize()` | "hello world" → "Hello world" | — |
|
|
2186
|
+
| `.titleCase()` | "hello world" → "Hello World" | — |
|
|
2187
|
+
| `.camelCase()` | "hello world" → "helloWorld" | — |
|
|
2188
|
+
| `.pascalCase()` | "hello world" → "HelloWorld" | — |
|
|
2189
|
+
| `.snakeCase()` | "hello world" → "hello_world" | — |
|
|
2190
|
+
| `.kebabCase()` | "hello world" → "hello-world" | — |
|
|
2065
2191
|
|
|
2066
2192
|
## Trim & whitespace mutators
|
|
2067
2193
|
|
|
2068
|
-
| Method
|
|
2069
|
-
|
|
2070
|
-
| `.trim(needle?)`
|
|
2071
|
-
| `.ltrim(needle?)`
|
|
2072
|
-
| `.rtrim(needle?)`
|
|
2073
|
-
| `.trimMultipleWhitespace()` | —
|
|
2074
|
-
| `.padStart(len, char?)`
|
|
2075
|
-
| `.padEnd(len, char?)`
|
|
2194
|
+
| Method | Args | Effect |
|
|
2195
|
+
| --------------------------- | ------------------ | ------------------- |
|
|
2196
|
+
| `.trim(needle?)` | default = space | trim both ends |
|
|
2197
|
+
| `.ltrim(needle?)` | — | trim left only |
|
|
2198
|
+
| `.rtrim(needle?)` | — | trim right only |
|
|
2199
|
+
| `.trimMultipleWhitespace()` | — | "a b" → "a b" |
|
|
2200
|
+
| `.padStart(len, char?)` | char default = " " | left-pad to length |
|
|
2201
|
+
| `.padEnd(len, char?)` | char default = " " | right-pad to length |
|
|
2076
2202
|
|
|
2077
2203
|
## Replace, append, modify mutators
|
|
2078
2204
|
|
|
2079
|
-
| Method
|
|
2080
|
-
|
|
2081
|
-
| `.replace(search, replace)`
|
|
2082
|
-
| `.replaceAll(search, replace)` | string or RegExp + string | —
|
|
2083
|
-
| `.append(suffix)`
|
|
2084
|
-
| `.prepend(prefix)`
|
|
2085
|
-
| `.reverse()`
|
|
2086
|
-
| `.repeat(count)`
|
|
2087
|
-
| `.truncate(maxLen, suffix?)`
|
|
2088
|
-
| `.mask(start, end?, char?)`
|
|
2205
|
+
| Method | Args | Example |
|
|
2206
|
+
| ------------------------------ | ------------------------- | -------------------------------------- |
|
|
2207
|
+
| `.replace(search, replace)` | string or RegExp + string | — |
|
|
2208
|
+
| `.replaceAll(search, replace)` | string or RegExp + string | — |
|
|
2209
|
+
| `.append(suffix)` | string | "foo" → "foobar" via `.append("bar")` |
|
|
2210
|
+
| `.prepend(prefix)` | string | "foo" → "barfoo" via `.prepend("bar")` |
|
|
2211
|
+
| `.reverse()` | — | "abc" → "cba" |
|
|
2212
|
+
| `.repeat(count)` | number | "ab" → "ababab" via `.repeat(3)` |
|
|
2213
|
+
| `.truncate(maxLen, suffix?)` | suffix default = "…" | "long text" → "long…" |
|
|
2214
|
+
| `.mask(start, end?, char?)` | char default = "*" | "1234567890" → "12******90" |
|
|
2089
2215
|
|
|
2090
2216
|
## Filter mutators
|
|
2091
2217
|
|
|
2092
|
-
| Method
|
|
2093
|
-
|
|
2094
|
-
| `.toAlpha()`
|
|
2095
|
-
| `.toAlphanumeric()`
|
|
2096
|
-
| `.removeSpecialCharacters()` | keep alphanumerics + whitespace
|
|
2097
|
-
| `.removeNumbers()`
|
|
2098
|
-
| `.stripTags()`
|
|
2099
|
-
| `.htmlEscape()`
|
|
2100
|
-
| `.unescapeHtml()`
|
|
2218
|
+
| Method | Effect |
|
|
2219
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
2220
|
+
| `.toAlpha()` | strip non-letters |
|
|
2221
|
+
| `.toAlphanumeric()` | strip non-alphanumerics |
|
|
2222
|
+
| `.removeSpecialCharacters()` | keep alphanumerics + whitespace |
|
|
2223
|
+
| `.removeNumbers()` | strip digits |
|
|
2224
|
+
| `.stripTags()` | strip HTML tags via a naive `<...>` regex — **not** an XSS sanitizer; `.safeHtml()` is a deprecated alias |
|
|
2225
|
+
| `.htmlEscape()` | `<` → `<` etc |
|
|
2226
|
+
| `.unescapeHtml()` | reverse `htmlEscape()` |
|
|
2101
2227
|
|
|
2102
2228
|
## Encoding mutators
|
|
2103
2229
|
|
|
2104
|
-
| Method
|
|
2105
|
-
|
|
2106
|
-
| `.base64Encode()` | utf8 → base64
|
|
2107
|
-
| `.base64Decode()` | base64 → utf8
|
|
2108
|
-
| `.urlEncode()`
|
|
2109
|
-
| `.urlDecode()`
|
|
2110
|
-
| `.slug()`
|
|
2111
|
-
| `.toString()`
|
|
2230
|
+
| Method | Effect |
|
|
2231
|
+
| ----------------- | --------------------------------- |
|
|
2232
|
+
| `.base64Encode()` | utf8 → base64 |
|
|
2233
|
+
| `.base64Decode()` | base64 → utf8 |
|
|
2234
|
+
| `.urlEncode()` | percent-encode |
|
|
2235
|
+
| `.urlDecode()` | percent-decode |
|
|
2236
|
+
| `.slug()` | "Hello World!" → "hello-world" |
|
|
2237
|
+
| `.toString()` | coerce non-string input to string |
|
|
2112
2238
|
|
|
2113
2239
|
## Mutator vs transformer
|
|
2114
2240
|
|
|
2115
|
-
The methods above are **all mutators** — they reshape the value
|
|
2241
|
+
The methods above are **all mutators** — they reshape the value _before_ validation rules run. If you want post-validation reshaping, attach via `.addTransformer(fn)` (see the `compose-seal-modifiers` topic).
|
|
2116
2242
|
|
|
2117
|
-
Practical implication: `v.string().min(3).trim()` runs `min(3)` against the
|
|
2243
|
+
Practical implication: `v.string().min(3).trim()` runs `min(3)` against the _un-trimmed_ input. To check trimmed length, mutate first: `v.string().trim().min(3)`.
|
|
2118
2244
|
|
|
2119
2245
|
## Common chains
|
|
2120
2246
|
|
|
2121
2247
|
```ts
|
|
2122
2248
|
// Email field
|
|
2123
|
-
v.string().email()
|
|
2249
|
+
v.string().email();
|
|
2124
2250
|
|
|
2125
2251
|
// Username
|
|
2126
|
-
v.string().min(3).max(30).alphanumeric().lowercase()
|
|
2252
|
+
v.string().min(3).max(30).alphanumeric().lowercase();
|
|
2127
2253
|
|
|
2128
2254
|
// Slug from title
|
|
2129
|
-
v.string().slug()
|
|
2255
|
+
v.string().slug();
|
|
2130
2256
|
|
|
2131
2257
|
// Strong password
|
|
2132
|
-
v.string().strongPassword(12)
|
|
2258
|
+
v.string().strongPassword(12);
|
|
2133
2259
|
|
|
2134
2260
|
// URL with strict format
|
|
2135
|
-
v.string().url().startsWith("https://")
|
|
2261
|
+
v.string().url().startsWith("https://");
|
|
2136
2262
|
|
|
2137
2263
|
// Tag-stripped HTML body (not XSS-safe — sanitize with DOMPurify/sanitize-html before rendering untrusted rich text)
|
|
2138
|
-
v.string().stripTags().min(1)
|
|
2264
|
+
v.string().stripTags().min(1);
|
|
2139
2265
|
|
|
2140
2266
|
// Masked phone for response
|
|
2141
|
-
v.string()
|
|
2267
|
+
v.string()
|
|
2268
|
+
.pattern(/^\+\d{8,}$/)
|
|
2269
|
+
.mask(3, -2);
|
|
2142
2270
|
|
|
2143
2271
|
// Optional with default
|
|
2144
|
-
v.string().email().default("guest@example.com").optional()
|
|
2272
|
+
v.string().email().default("guest@example.com").optional();
|
|
2145
2273
|
```
|
|
2146
2274
|
|
|
2275
|
+
The standalone `replaceMutator` and `replaceAllMutator` preserve the stringified
|
|
2276
|
+
value when called without a context or replacement options.
|
|
2277
|
+
|
|
2147
2278
|
|
|
2148
2279
|
## seal-basics `@warlock.js/seal/seal-basics/SKILL.md`
|
|
2149
2280
|
|
|
@@ -2154,7 +2285,7 @@ description: "Start here for @warlock.js/seal schemas, validation, parsing, and
|
|
|
2154
2285
|
|
|
2155
2286
|
# Validate with seal
|
|
2156
2287
|
|
|
2157
|
-
Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators
|
|
2288
|
+
Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators _and_ type-level shapes via `Infer<typeof schema>`, and ship `~standard` so any Standard-Schema-aware consumer accepts them directly.
|
|
2158
2289
|
|
|
2159
2290
|
> This skill is the seal **map** — read it first, then load the specific skill for the task.
|
|
2160
2291
|
|
|
@@ -2182,9 +2313,9 @@ type User = Infer<typeof userSchema>;
|
|
|
2182
2313
|
|
|
2183
2314
|
const result = await validate(userSchema, input);
|
|
2184
2315
|
if (result.isValid) {
|
|
2185
|
-
result.data;
|
|
2316
|
+
result.data; // typed, post-transformer
|
|
2186
2317
|
} else {
|
|
2187
|
-
result.errors;
|
|
2318
|
+
result.errors; // [{ type, error, input }, ...]
|
|
2188
2319
|
}
|
|
2189
2320
|
```
|
|
2190
2321
|
|
|
@@ -2218,7 +2349,7 @@ The 12 things that are true in every seal use:
|
|
|
2218
2349
|
2. **Every factory return is a `StandardSchemaV1<Infer.Output<T>>`.** Pass seal schemas straight into `StandardSchemaV1<T>`-typed slots — no casts.
|
|
2219
2350
|
3. **Two inference helpers — `Infer.Input<T>` and `Infer.Output<T>`.** Bare `Infer<T>` is an alias for `Infer.Input<T>` (the dominant usage: HTTP bodies, DTOs, form payloads). Use `Infer.Output<T>` for validated state (Cascade `Model<>` params, post-`validate()` data). See [the rules below](#inferinput-vs-inferoutput).
|
|
2220
2351
|
4. **Fields are required by default.** Mark optional explicitly: `.optional()`. Skip `.required()` — canonical seal style omits the redundant call; inferred types already show what's required.
|
|
2221
|
-
5. **`validate(schema, data)` never throws.** Returns `
|
|
2352
|
+
5. **`validate(schema, data)` never throws.** Returns `{ isValid, data, errors }`, narrowed on `isValid`: on success `data` is `Infer.Output<typeof schema>`, on failure it is `undefined` (a bare `BaseValidator`/`ObjectValidator` annotation keeps `data: any`). See the `handle-seal-errors` topic.
|
|
2222
2353
|
6. **Validators are immutable by default.** Every chain method (`.min(3)`, `.email()`, `.optional()`, …) returns a clone. Toggle with the `.mutable` getter.
|
|
2223
2354
|
7. **Two pipelines: mutators (pre-validation), transformers (post-validation).** Order: `default → mutators → required check → required-condition rule → other rules → transformers → data`. `.catch(fallback)` rescues any failure on leaf validators.
|
|
2224
2355
|
8. **Cross-field rules need a `v.object` parent.** Standalone scalar validators have no siblings to resolve against.
|
|
@@ -2240,13 +2371,13 @@ const schema = v.object({
|
|
|
2240
2371
|
status: v.enum(Status).optional().default(Status.ACTIVE),
|
|
2241
2372
|
});
|
|
2242
2373
|
|
|
2243
|
-
type In
|
|
2374
|
+
type In = Infer.Input<typeof schema>;
|
|
2244
2375
|
// { email?: string; status?: Status } ← caller may omit both
|
|
2245
2376
|
|
|
2246
2377
|
type Out = Infer.Output<typeof schema>;
|
|
2247
2378
|
// { email?: string; status: Status } ← default fired for status
|
|
2248
2379
|
|
|
2249
|
-
type Default = Infer<typeof schema>;
|
|
2380
|
+
type Default = Infer<typeof schema>; // alias for Infer.Input
|
|
2250
2381
|
```
|
|
2251
2382
|
|
|
2252
2383
|
Both flavours widen with `| null` when `.nullable()` is set.
|
|
@@ -2258,15 +2389,15 @@ Both flavours widen with `| null` when `.nullable()` is set.
|
|
|
2258
2389
|
|
|
2259
2390
|
## Pick a skill
|
|
2260
2391
|
|
|
2261
|
-
| If the task is about…
|
|
2262
|
-
|
|
|
2263
|
-
| Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`)
|
|
2264
|
-
| Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas
|
|
2265
|
-
| Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules
|
|
2266
|
-
| Reading `ValidationResult`, branching on `error.type`, error message customization, translation
|
|
2267
|
-
| Generating JSON Schema for OpenAI strict / OpenAPI / draft-07
|
|
2268
|
-
| Why a `StandardSchemaV1<T>` slot accepts/rejects a schema,
|
|
2269
|
-
| Authoring custom seal plugins to add validator methods
|
|
2392
|
+
| If the task is about… | Load |
|
|
2393
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
2394
|
+
| Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`) | The `pick-seal-primitive` topic |
|
|
2395
|
+
| Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas | The `define-structural-shape` topic |
|
|
2396
|
+
| Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules | The `compose-seal-modifiers` topic |
|
|
2397
|
+
| Reading `ValidationResult`, branching on `error.type`, error message customization, translation | The `handle-seal-errors` topic |
|
|
2398
|
+
| Generating JSON Schema for OpenAI strict / OpenAPI / draft-07 | The `generate-json-schema` topic |
|
|
2399
|
+
| Why a `StandardSchemaV1<T>` slot accepts/rejects a schema, object input/output class parameters and primitive factory intersections, `Result<unknown>` errors | The `bridge-standard-schema` topic |
|
|
2400
|
+
| Authoring custom seal plugins to add validator methods | The `extend-seal-with-plugins` topic |
|
|
2270
2401
|
|
|
2271
2402
|
## Things NOT to do
|
|
2272
2403
|
|