@@ -17,6 +17,8 @@ This small library provides a simple schema validation system for JavaScript/Typ
1717 - [ Full Extensions] ( #full-extensions-esmjschemafull )
1818- [ API Reference Summary] ( #api-reference-summary )
1919- [ Schema Types] ( #schema-types )
20+ - [ s.coerce] ( #scoerce )
21+ - [ s.cast] ( #scast )
2022- [ Schema Methods] ( #schema-methods )
2123 - [ parse] ( #parsevalue-parseoptions )
2224 - [ safeParse] ( #safeparsevalue-parseoptions )
@@ -449,6 +451,16 @@ const schema = s.object({
449451- ` s.coerce.boolean() ` - Coerce any value to boolean, then validate
450452- ` s.coerce.date() ` - Coerce any value to Date, then validate (fails for invalid dates)
451453
454+ ### Cast
455+
456+ Semantic casting that understands common string representations and rejects ambiguous inputs:
457+
458+ - ` s.cast.boolean() ` - Cast to boolean; understands ` 'true'/'false' ` , ` 'yes'/'no' ` , ` 'on'/'off' ` , ` '1'/'0' ` (case-insensitive); rejects ` null ` /` undefined ` /unrecognised strings
459+ - ` s.cast.number() ` - Cast to number; trims whitespace from strings, accepts booleans (` true ` →1, ` false ` →0); rejects ` null ` /` undefined ` /empty strings
460+ - ` s.cast.string() ` - Cast to string; accepts strings, finite numbers, and booleans; rejects ` null ` /` undefined ` /objects/` NaN ` /` Infinity `
461+ - ` s.cast.date() ` - Cast to Date; accepts ISO strings, finite timestamps, and existing Dates; rejects ` null ` /` undefined ` /booleans/empty strings
462+ - ` s.cast.json(schema) ` - Parse a JSON string and validate the result against a schema; non-string inputs pass through directly; malformed JSON returns a proper validation failure
463+
452464### Transformations
453465
454466- ` .transform(fn) ` - Transform value
@@ -773,6 +785,83 @@ s.coerce.number().refine((v) => v > 0, { message: 'Must be positive' }).parse('5
773785s .coerce .number ({ message: ' Expected a numeric value' }).parse (' bad' ); // throws: Expected a numeric value
774786```
775787
788+ #### ` s.cast `
789+
790+ Programmer-friendly semantic casting. Unlike ` s.coerce ` (raw JS constructors), ` s.cast ` understands
791+ common string representations and rejects ambiguous inputs like ` null ` , ` undefined ` , and empty strings.
792+
793+ | Method | Accepted inputs | Rejects |
794+ | ---| ---| ---|
795+ | ` s.cast.string(options?) ` | strings, finite numbers, booleans | ` null ` , ` undefined ` , objects, ` NaN ` , ` Infinity ` |
796+ | ` s.cast.number(options?) ` | numbers (incl. booleans ` true ` /` false ` →1/0), trimmed numeric strings | ` null ` , ` undefined ` , empty strings, non-numeric strings |
797+ | ` s.cast.boolean(options?) ` | booleans, ` 1 ` /` 0 ` , ` 'true'/'false' ` , ` 'yes'/'no' ` , ` 'on'/'off' ` , ` '1'/'0' ` | ` null ` , ` undefined ` , unrecognised strings, other numbers |
798+ | ` s.cast.date(options?) ` | ` Date ` objects, ISO strings, finite integer timestamps | ` null ` , ` undefined ` , booleans, empty strings, invalid date strings |
799+ | ` s.cast.json(schema, options?) ` | JSON strings (parsed), any non-string value (pass-through) | malformed JSON strings |
800+
801+ ** Key differences from ` s.coerce ` :**
802+
803+ | Input | ` s.coerce.boolean() ` | ` s.cast.boolean() ` |
804+ | ---| ---| ---|
805+ | ` 'false' ` | ` true ` (non-empty string!) | ` false ` |
806+ | ` 'yes' ` / ` 'no' ` | ` true ` / ` true ` | ` true ` / ` false ` |
807+ | ` null ` | ` false ` | throws |
808+
809+ | Input | ` s.coerce.number() ` | ` s.cast.number() ` |
810+ | ---| ---| ---|
811+ | ` null ` | ` 0 ` | throws |
812+ | ` '' ` | ` 0 ` | throws |
813+
814+ | Input | ` s.coerce.string() ` | ` s.cast.string() ` |
815+ | ---| ---| ---|
816+ | ` null ` | ` 'null' ` | throws |
817+ | ` undefined ` | ` 'undefined' ` | throws |
818+
819+ ``` typescript
820+ // boolean
821+ s .cast .boolean ().parse (' false' ); // false — unlike coerce!
822+ s .cast .boolean ().parse (' yes' ); // true
823+ s .cast .boolean ().parse (' on' ); // true
824+ s .cast .boolean ().parse (' OFF' ); // false (case-insensitive)
825+ s .cast .boolean ().parse (1 ); // true
826+ s .cast .boolean ().parse (0 ); // false
827+ s .cast .boolean ().parse (' hello' ); // throws: Cannot cast "hello" to boolean...
828+ s .cast .boolean ().parse (null ); // throws
829+
830+ // number
831+ s .cast .number ().parse (' 42' ); // 42
832+ s .cast .number ().parse (' 3.14 ' ); // 3.14 — trims whitespace
833+ s .cast .number ().parse (true ); // 1
834+ s .cast .number ().parse (false ); // 0
835+ s .cast .number ().parse (null ); // throws: Cannot cast "null" to a number...
836+ s .cast .number ().parse (' ' ); // throws
837+
838+ // string
839+ s .cast .string ().parse (123 ); // '123'
840+ s .cast .string ().parse (true ); // 'true'
841+ s .cast .string ().parse (false ); // 'false'
842+ s .cast .string ().parse (null ); // throws: Cannot cast "null" to string...
843+ s .cast .string ().parse (NaN ); // throws
844+
845+ // date
846+ s .cast .date ().parse (' 2024-01-01' ); // Date object
847+ s .cast .date ().parse (1704067200000 ); // Date object
848+ s .cast .date ().parse (null ); // throws: Cannot cast "null" to a valid date.
849+ s .cast .date ().parse (true ); // throws
850+
851+ // All schema methods chain normally:
852+ s .cast .number ().refine ((v ) => v > 0 , { message: ' Must be positive' }).parse (' 5' ); // 5
853+
854+ // Custom error message:
855+ s .cast .boolean ({ message: ' Must be a boolean flag' }).parse (' maybe' ); // throws: Must be a boolean flag
856+
857+ // json
858+ s .cast .json (s .object ({ name: s .string () })).parse (' {"name":"Alice"}' ); // { name: 'Alice' }
859+ s .cast .json (s .array (s .number ())).parse (' [1,2,3]' ); // [1, 2, 3]
860+ s .cast .json (s .object ({ name: s .string () })).parse ({ name: ' Alice' }); // { name: 'Alice' } — pass-through
861+ s .cast .json (s .number ()).safeParse (' not json' ); // { success: false, error: ... }
862+ s .cast .json (s .number (), { message: ' Invalid JSON' }).parse (' bad' ); // throws: Invalid JSON
863+ ```
864+
776865### Schema Methods
777866
778867#### ` parse(value, parseOptions?) `
@@ -1404,6 +1493,7 @@ const userSchema = s.object({
14041493| Email validation | ` .email() ` built-in | Custom extension (see [ Extending Schemas] ( #extending-schemas ) ) |
14051494| Error format | Native Error | Plain object ` { success, error, errors } ` |
14061495| Coerce | ` z.coerce.number() ` | ` s.coerce.number() ` |
1496+ | Smart cast | No direct equivalent | ` s.cast.number() ` — rejects nulls, understands ` 'yes'/'no' ` , etc. |
14071497
14081498** Migration Tips:**
14091499
0 commit comments