1/**
2 * Builds two-way conversions used by schemas.
3 *
4 * A `Transformation<T, E>` describes how to decode an encoded value into a
5 * decoded value and how to encode it back again. Schema APIs use
6 * transformations to connect two representations, such as a string and a
7 * number, a JSON value and a richer TypeScript value, or a form field and an
8 * application value. This module includes transformation and middleware types,
9 * constructors for pure or effectful conversions, and common conversions used
10 * by the Schema module.
11 *
12 * @since 4.0.0
13 */
14
15import * as BigDecimal from "./BigDecimal.ts"
16import * as ByteSize from "./ByteSize.ts"
17import * as DateTime from "./DateTime.ts"
18import * as Duration from "./Duration.ts"
19import * as Effect from "./Effect.ts"
20import { format, formatDate, formatJson } from "./Formatter.ts"
21import { dual } from "./Function.ts"
22import * as Option from "./Option.ts"
23import * as Pipeable from "./Pipeable.ts"
24import * as Predicate from "./Predicate.ts"
25import type { ErrorOptions, Json } from "./Schema.ts"
26import type * as SchemaAST from "./SchemaAST.ts"
27import * as SchemaGetter from "./SchemaGetter.ts"
28import * as SchemaIssue from "./SchemaIssue.ts"
29
30/**
31 * Middleware that wraps the entire parsing `Effect` pipeline for both
32 * decode and encode directions.
33 *
34 * **When to use**
35 *
36 * Use when you need a schema middleware to catch or recover from parsing
37 * errors (e.g. `Schema.catchDecoding`), run side effects around the parsing
38 * pipeline, or access the full `Effect` rather than a single decoded value.
39 *
40 * **Details**
41 *
42 * Unlike `Transformation`, which operates on individual values via `Getter`,
43 * `Middleware` receives the full `Effect` produced by the inner schema and can
44 * intercept, modify, retry, or replace it.
45 *
46 * - `decode` receives an `Effect<Option<E>, Issue, RDE>` and returns
47 * `Effect<Option<T>, Issue, RDT>`.
48 * - `encode` receives an `Effect<Option<T>, Issue, RET>` and returns
49 * `Effect<Option<E>, Issue, REE>`.
50 * - `flip()` swaps the decode and encode functions, producing a
51 * `Middleware<E, T, ...>`.
52 * - Middleware values implement `Pipeable`.
53 *
54 * Typically constructed indirectly via `Schema.middlewareDecoding` or
55 * `Schema.middlewareEncoding` rather than by using `new Middleware` directly.
56 *
57 * **Example** (Creating a middleware that falls back on decode failure)
58 *
59 * ```ts import.meta.vitest
60 * import { Effect, Option, SchemaIssue, SchemaTransformation } from "effect"
61 *
62 * const fallback = new SchemaTransformation.Middleware<string, string, never, never, never, never>(
63 * (effect) => Effect.catch(effect, () => Effect.succeed(Option.some("fallback"))),
64 * (effect) => effect
65 * )
66 * const issue = new SchemaIssue.InvalidValue({ message: "Missing value" })
67 * await Effect.runPromise(fallback.decode(Effect.fail(issue), {})) // => Option.some("fallback")
68 * ```
69 *
70 * @see {@link Transformation} — value-level bidirectional transformation
71 *
72 * @category models
73 * @since 4.0.0
74 */
75export interface Middleware<in out T, in out E, RDE, RDT, RET, REE> extends Pipeable.Pipeable {
76 readonly _tag: "Middleware"
77 readonly decode: (
78 effect: Effect.Effect<Option.Option<E>, SchemaIssue.Issue, RDE>,
79 options: SchemaAST.ParseOptions
80 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RDT>
81 readonly encode: (
82 effect: Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RET>,
83 options: SchemaAST.ParseOptions
84 ) => Effect.Effect<Option.Option<E>, SchemaIssue.Issue, REE>
85 flip(): Middleware<E, T, RET, REE, RDE, RDT>
86}
87
88/**
89 * Constructs schema middleware from its decode and encode functions.
90 *
91 * @category constructors
92 * @since 4.0.0
93 */
94export const Middleware: new<T, E, RDE, RDT, RET, REE>(
95 decode: (
96 effect: Effect.Effect<Option.Option<E>, SchemaIssue.Issue, RDE>,
97 options: SchemaAST.ParseOptions
98 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RDT>,
99 encode: (
100 effect: Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RET>,
101 options: SchemaAST.ParseOptions
102 ) => Effect.Effect<Option.Option<E>, SchemaIssue.Issue, REE>
103) => Middleware<T, E, RDE, RDT, RET, REE> = class<in out T, in out E, RDE, RDT, RET, REE> extends Pipeable.Class {
104 readonly _tag = "Middleware"
105 readonly decode: (
106 effect: Effect.Effect<Option.Option<E>, SchemaIssue.Issue, RDE>,
107 options: SchemaAST.ParseOptions
108 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RDT>
109 readonly encode: (
110 effect: Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RET>,
111 options: SchemaAST.ParseOptions
112 ) => Effect.Effect<Option.Option<E>, SchemaIssue.Issue, REE>
113
114 constructor(
115 decode: (
116 effect: Effect.Effect<Option.Option<E>, SchemaIssue.Issue, RDE>,
117 options: SchemaAST.ParseOptions
118 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RDT>,
119 encode: (
120 effect: Effect.Effect<Option.Option<T>, SchemaIssue.Issue, RET>,
121 options: SchemaAST.ParseOptions
122 ) => Effect.Effect<Option.Option<E>, SchemaIssue.Issue, REE>
123 ) {
124 super()
125 this.decode = decode
126 this.encode = encode
127 }
128 flip(): Middleware<E, T, RET, REE, RDE, RDT> {
129 return new Middleware(this.encode, this.decode)
130 }
131}
132
133const TypeId = "~effect/SchemaTransformation/Transformation"
134
135/**
136 * Represents a bidirectional transformation between a decoded type `T` and an encoded
137 * type `E`, built from a pair of `Getter`s.
138 *
139 * **When to use**
140 *
141 * Use when you need a schema transformation that defines how a schema converts
142 * between two representations.
143 * - You want to compose multiple transformations into a pipeline.
144 * - You want to flip a transformation to swap decode/encode.
145 *
146 * **Details**
147 *
148 * This is the primary building block for `Schema.decodeTo`, `Schema.encodeTo`,
149 * `Schema.decode`, `Schema.encode`, and `Schema.link`. Each direction is a
150 * `SchemaGetter.Getter` that handles optionality, failure, and Effect services.
151 *
152 * - Immutable — `flip()` and {@link composeTransformation} return new instances.
153 * - Transformation values implement `Pipeable`.
154 * - `flip()` swaps the decode and encode getters.
155 * - `composeTransformation(self, other)` chains: `self.decode` then `other.decode` for decoding,
156 * `other.encode` then `self.encode` for encoding.
157 *
158 * **Example** (Composing two transformations)
159 *
160 * ```ts import.meta.vitest
161 * import { SchemaTransformation } from "effect"
162 *
163 * const trimAndLower = SchemaTransformation.composeTransformation(
164 * SchemaTransformation.trim(),
165 * SchemaTransformation.toLowerCase()
166 * )
167 * trimAndLower._tag // => "Transformation"
168 * ```
169 *
170 * @see {@link makeTransformation} — construct from `{ decode, encode }` getters
171 * @see {@link transform} — construct from pure functions
172 * @see {@link transformEffect} — construct from effectful functions
173 * @see {@link Middleware} — effect-pipeline-level alternative
174 *
175 * @category models
176 * @since 4.0.0
177 */
178export interface Transformation<in out T, in out E, RD = never, RE = never> extends Pipeable.Pipeable {
179 readonly [TypeId]: typeof TypeId
180 readonly _tag: "Transformation"
181 readonly decode: SchemaGetter.Getter<T, E, RD>
182 readonly encode: SchemaGetter.Getter<E, T, RE>
183 flip(): Transformation<E, T, RE, RD>
184}
185
186/**
187 * Constructs a bidirectional schema transformation from its decode and encode getters.
188 *
189 * @category constructors
190 * @since 4.0.0
191 */
192export const Transformation: new<T, E, RD = never, RE = never>(
193 decode: SchemaGetter.Getter<T, E, RD>,
194 encode: SchemaGetter.Getter<E, T, RE>
195) => Transformation<T, E, RD, RE> = class<in out T, in out E, RD = never, RE = never> extends Pipeable.Class {
196 readonly [TypeId] = TypeId
197 readonly _tag = "Transformation"
198 readonly decode: SchemaGetter.Getter<T, E, RD>
199 readonly encode: SchemaGetter.Getter<E, T, RE>
200
201 constructor(
202 decode: SchemaGetter.Getter<T, E, RD>,
203 encode: SchemaGetter.Getter<E, T, RE>
204 ) {
205 super()
206 this.decode = decode
207 this.encode = encode
208 }
209 flip(): Transformation<E, T, RE, RD> {
210 return new Transformation(this.encode, this.decode)
211 }
212}
213
214/**
215 * Composes two schema transformations into a single bidirectional conversion.
216 *
217 * **When to use**
218 *
219 * Use when decoding and encoding require the same sequence of conversion
220 * steps in opposite directions.
221 *
222 * **Details**
223 *
224 * Decoding applies `self.decode` followed by `other.decode`. Encoding applies
225 * `other.encode` followed by `self.encode`. The function supports both
226 * `composeTransformation(self, other)` and `composeTransformation(other)(self)`.
227 *
228 * **Example** (Trimming and lowercasing a string)
229 *
230 * ```ts import.meta.vitest
231 * import { Schema, SchemaTransformation } from "effect"
232 *
233 * const transformation = SchemaTransformation.composeTransformation(
234 * SchemaTransformation.trim(),
235 * SchemaTransformation.toLowerCase()
236 * )
237 * const schema = Schema.String.pipe(Schema.decode(transformation))
238 *
239 * Schema.decodeUnknownSync(schema)(" HELLO ") // => "hello"
240 * ```
241 *
242 * @category combining
243 * @since 4.0.0
244 */
245export const composeTransformation: {
246 <T, T2, RD2, RE2>(
247 other: Transformation<T2, T, RD2, RE2>
248 ): <E, RD, RE>(self: Transformation<T, E, RD, RE>) => Transformation<T2, E, RD | RD2, RE | RE2>
249 <T, E, RD, RE, T2, RD2, RE2>(
250 self: Transformation<T, E, RD, RE>,
251 other: Transformation<T2, T, RD2, RE2>
252 ): Transformation<T2, E, RD | RD2, RE | RE2>
253} = dual(2, <T, E, RD, RE, T2, RD2, RE2>(
254 self: Transformation<T, E, RD, RE>,
255 other: Transformation<T2, T, RD2, RE2>
256): Transformation<T2, E, RD | RD2, RE | RE2> =>
257 new Transformation(
258 SchemaGetter.compose(self.decode, other.decode),
259 SchemaGetter.compose(other.encode, self.encode)
260 ))
261
262/**
263 * Returns `true` if `u` is a `Transformation` instance.
264 *
265 * **When to use**
266 *
267 * Use to check whether a value is already a schema transformation before
268 * wrapping it.
269 *
270 * **Details**
271 *
272 * - Pure predicate, no side effects.
273 * - Acts as a TypeScript type guard.
274 *
275 * **Example** (Checking a value)
276 *
277 * ```ts import.meta.vitest
278 * import { SchemaTransformation } from "effect"
279 *
280 * SchemaTransformation.isTransformation(SchemaTransformation.trim()) // => true
281 * SchemaTransformation.isTransformation({ decode: null, encode: null }) // => false
282 * ```
283 *
284 * @see {@link Transformation}
285 * @see {@link makeTransformation}
286 *
287 * @category guards
288 * @since 4.0.0
289 */
290export function isTransformation(u: unknown): u is Transformation<any, any, unknown, unknown> {
291 return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId
292}
293
294/**
295 * Constructs a `Transformation` from an object with `decode` and `encode`
296 * `Getter`s. If the input is already a `Transformation`, returns it as-is.
297 *
298 * **When to use**
299 *
300 * Use when you already have schema getter instances and want to pair them into
301 * a schema transformation.
302 * - You want idempotent wrapping (won't double-wrap).
303 *
304 * **Details**
305 *
306 * - Returns the input unchanged if it is already a `Transformation`.
307 *
308 * **Example** (Wrapping existing getters)
309 *
310 * ```ts import.meta.vitest
311 * import { SchemaGetter, SchemaTransformation } from "effect"
312 *
313 * const t = SchemaTransformation.makeTransformation({
314 * decode: SchemaGetter.transform<number, string>((s) => Number(s)),
315 * encode: SchemaGetter.transform<string, number>((n) => String(n))
316 * })
317 * t._tag // => "Transformation"
318 * ```
319 *
320 * @see {@link transform} — simpler constructor from pure functions
321 * @see {@link transformEffect} — constructor from effectful functions
322 * @see {@link Transformation}
323 *
324 * @category constructors
325 * @since 3.10.0
326 */
327export const makeTransformation = <T, E, RD = never, RE = never>(options: {
328 readonly decode: SchemaGetter.Getter<T, E, RD>
329 readonly encode: SchemaGetter.Getter<E, T, RE>
330}): Transformation<T, E, RD, RE> => {
331 if (isTransformation(options)) {
332 return options as any
333 }
334 return new Transformation(options.decode, options.encode)
335}
336
337/**
338 * Creates a `Transformation` from effectful decode and encode functions.
339 *
340 * **When to use**
341 *
342 * Use when you need a schema transformation that may fail or require Effect
343 * services.
344 *
345 * **Details**
346 *
347 * - Each function receives the input value and `ParseOptions`.
348 * - Must return an `Effect` that succeeds with the output or fails with `Issue`.
349 * - Skips `None` inputs (missing keys) — functions are only called on present values.
350 *
351 * **Example** (Parsing a date string that can fail)
352 *
353 * ```ts import.meta.vitest
354 * import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect"
355 *
356 * const DateFromString = Schema.String.pipe(
357 * Schema.decodeTo(
358 * Schema.Date,
359 * SchemaTransformation.transformEffect({
360 * decode: (s, options) => {
361 * const d = new Date(s)
362 * return isNaN(d.getTime())
363 * ? Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid date" }, s, options))
364 * : Effect.succeed(d)
365 * },
366 * encode: (d) => Effect.succeed(d.toISOString())
367 * })
368 * )
369 * )
370 * Schema.decodeSync(DateFromString)("2024-01-01").toISOString() // => "2024-01-01T00:00:00.000Z"
371 * ```
372 *
373 * @see {@link transform} — for infallible, pure transformations
374 * @see {@link transformOptional} — for transformations that handle missing keys
375 * @see {@link makeTransformation} — for transformations from existing Getters
376 *
377 * @category transforming
378 * @since 3.10.0
379 */
380export function transformEffect<T, E, RD = never, RE = never>(options: {
381 readonly decode: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue, RD>
382 readonly encode: (t: T, options: SchemaAST.ParseOptions) => Effect.Effect<E, SchemaIssue.Issue, RE>
383}): Transformation<T, E, RD, RE> {
384 return new Transformation(
385 SchemaGetter.transformEffect(options.decode),
386 SchemaGetter.transformEffect(options.encode)
387 )
388}
389
390/**
391 * Creates a `Transformation` from pure (sync, infallible) decode and encode
392 * functions.
393 *
394 * **When to use**
395 *
396 * Use when you need an infallible schema transformation that does not require
397 * Effect services.
398 *
399 * **Details**
400 *
401 * - Each function receives the input and returns the output directly.
402 * - Skips `None` inputs (missing keys) — functions are only called on present values.
403 * - Does not allocate Effects internally; uses optimized sync path.
404 *
405 * **Example** (Converting between cents and dollars)
406 *
407 * ```ts import.meta.vitest
408 * import { Schema, SchemaTransformation } from "effect"
409 *
410 * const CentsFromDollars = Schema.Number.pipe(
411 * Schema.decodeTo(
412 * Schema.Number,
413 * SchemaTransformation.transform({
414 * decode: (dollars) => dollars * 100,
415 * encode: (cents) => cents / 100
416 * })
417 * )
418 * )
419 * Schema.decodeSync(CentsFromDollars)(2.5) // => 250
420 * ```
421 *
422 * @see {@link transformEffect} — for fallible or effectful transformations
423 * @see {@link transformOptional} — for transformations that handle missing keys
424 * @see {@link passthrough} — when no conversion is needed
425 *
426 * @category transforming
427 * @since 3.10.0
428 */
429export function transform<T, E>(options: {
430 readonly decode: (input: E) => T
431 readonly encode: (input: T) => E
432}): Transformation<T, E> {
433 return new Transformation(
434 SchemaGetter.transform(options.decode),
435 SchemaGetter.transform(options.encode)
436 )
437}
438
439/**
440 * Creates a `Transformation` where decode and encode operate on `Option`
441 * values, giving full control over missing-key handling.
442 *
443 * **When to use**
444 *
445 * Use when you need a schema transformation to produce or consume `Option.None`
446 * for absent keys.
447 * - You are working with optional struct fields.
448 *
449 * **Details**
450 *
451 * - Each function receives `Option<input>` and returns `Option<output>`.
452 * - `Option.None` input means the key is absent; returning `Option.None`
453 * omits the key from the output.
454 * - Pure and synchronous.
455 *
456 * **Example** (Converting an optional key to Option)
457 *
458 * ```ts import.meta.vitest
459 * import { Option, Schema, SchemaTransformation } from "effect"
460 *
461 * const schema = Schema.Struct({
462 * a: Schema.optionalKey(Schema.Number).pipe(
463 * Schema.decodeTo(
464 * Schema.Option(Schema.Number),
465 * SchemaTransformation.transformOptional({
466 * decode: Option.some,
467 * encode: Option.flatten
468 * })
469 * )
470 * )
471 * })
472 * Schema.decodeSync(schema)({}).a // => Option.none()
473 * ```
474 *
475 * @see {@link transform} — when you don't need Option-level control
476 * @see {@link optionFromOptionalKey} — built-in for the common optional-key-to-Option pattern
477 * @see {@link optionFromOptional} — built-in for optional (undefined) to Option
478 *
479 * @category transforming
480 * @since 4.0.0
481 */
482export function transformOptional<T, E>(options: {
483 readonly decode: (input: Option.Option<E>) => Option.Option<T>
484 readonly encode: (input: Option.Option<T>) => Option.Option<E>
485}): Transformation<T, E> {
486 return new Transformation(
487 SchemaGetter.transformOptional(options.decode),
488 SchemaGetter.transformOptional(options.encode)
489 )
490}
491
492/**
493 * Transforms strings by trimming whitespace on decode.
494 * Encode is passthrough (no change).
495 *
496 * **When to use**
497 *
498 * Use when you need a schema transformation to normalize user input by
499 * stripping leading/trailing whitespace.
500 *
501 * **Details**
502 *
503 * Decoding applies `String.prototype.trim()`. Encoding is passthrough and
504 * returns the string unchanged. This is not round-trippable if the original had
505 * whitespace.
506 *
507 * **Example** (Trimming on decode)
508 *
509 * ```ts import.meta.vitest
510 * import { Schema, SchemaTransformation } from "effect"
511 *
512 * const Trimmed = Schema.String.pipe(
513 * Schema.decode(SchemaTransformation.trim())
514 * )
515 * Schema.decodeSync(Trimmed)(" hello ") // => "hello"
516 * ```
517 *
518 * @see {@link toLowerCase}
519 * @see {@link toUpperCase}
520 * @see {@link snakeToCamel}
521 *
522 * @category transforming
523 * @since 4.0.0
524 */
525export function trim(): Transformation<string, string> {
526 return new Transformation(
527 SchemaGetter.trim(),
528 SchemaGetter.passthrough()
529 )
530}
531
532/**
533 * Transforms strings by converting snake_case to camelCase
534 * on decode and camelCase to snake_case on encode.
535 *
536 * **When to use**
537 *
538 * Use when you need a schema transformation to convert API field names between
539 * snake_case and camelCase conventions.
540 *
541 * **Details**
542 *
543 * Decoding converts values such as `"my_field_name"` to `"myFieldName"`.
544 * Encoding converts values such as `"myFieldName"` back to `"my_field_name"`.
545 * The transformation is round-trippable for standard snake_case and camelCase.
546 *
547 * **Example** (Converting snake case to camel case)
548 *
549 * ```ts import.meta.vitest
550 * import { Schema, SchemaTransformation } from "effect"
551 *
552 * const SnakeToCamel = Schema.String.pipe(
553 * Schema.decode(SchemaTransformation.snakeToCamel())
554 * )
555 * Schema.decodeSync(SnakeToCamel)("user_name") // => "userName"
556 * ```
557 *
558 * @see {@link trim}
559 * @see {@link toLowerCase}
560 *
561 * @category transforming
562 * @since 4.0.0
563 */
564export function snakeToCamel(): Transformation<string, string> {
565 return new Transformation(
566 SchemaGetter.snakeToCamel(),
567 SchemaGetter.camelToSnake()
568 )
569}
570
571/**
572 * Transforms strings by lowercasing on decode.
573 * Encode is passthrough.
574 *
575 * **When to use**
576 *
577 * Use when you need a schema transformation to normalize strings to lowercase
578 * (e.g. email addresses).
579 *
580 * **Details**
581 *
582 * Decoding applies `String.prototype.toLowerCase()`. Encoding is passthrough.
583 * This is not round-trippable if the original had uppercase characters.
584 *
585 * **Example** (Lowercasing on decode)
586 *
587 * ```ts import.meta.vitest
588 * import { Schema, SchemaTransformation } from "effect"
589 *
590 * const Lowered = Schema.String.pipe(
591 * Schema.decode(SchemaTransformation.toLowerCase())
592 * )
593 * Schema.decodeSync(Lowered)("HELLO") // => "hello"
594 * ```
595 *
596 * @see {@link toUpperCase}
597 * @see {@link trim}
598 *
599 * @category transforming
600 * @since 4.0.0
601 */
602export function toLowerCase(): Transformation<string, string> {
603 return new Transformation(
604 SchemaGetter.toLowerCase(),
605 SchemaGetter.passthrough()
606 )
607}
608
609/**
610 * Transforms strings by uppercasing on decode.
611 * Encode is passthrough.
612 *
613 * **When to use**
614 *
615 * Use when you need a schema transformation to normalize strings to uppercase
616 * (e.g. country codes).
617 *
618 * **Details**
619 *
620 * Decoding applies `String.prototype.toUpperCase()`. Encoding is passthrough.
621 * This is not round-trippable if the original had lowercase characters.
622 *
623 * **Example** (Uppercasing on decode)
624 *
625 * ```ts import.meta.vitest
626 * import { Schema, SchemaTransformation } from "effect"
627 *
628 * const Uppered = Schema.String.pipe(
629 * Schema.decode(SchemaTransformation.toUpperCase())
630 * )
631 * Schema.decodeSync(Uppered)("hello") // => "HELLO"
632 * ```
633 *
634 * @see {@link toLowerCase}
635 * @see {@link trim}
636 *
637 * @category transforming
638 * @since 4.0.0
639 */
640export function toUpperCase(): Transformation<string, string> {
641 return new Transformation(
642 SchemaGetter.toUpperCase(),
643 SchemaGetter.passthrough()
644 )
645}
646
647/**
648 * Transforms strings by capitalizing the first character on
649 * decode. Encode is passthrough.
650 *
651 * **When to use**
652 *
653 * Use when you need a schema transformation to normalize display names or
654 * titles.
655 *
656 * **Details**
657 *
658 * Decoding uppercases the first character and leaves the rest unchanged.
659 * Encoding is passthrough.
660 *
661 * **Example** (Capitalizing on decode)
662 *
663 * ```ts import.meta.vitest
664 * import { Schema, SchemaTransformation } from "effect"
665 *
666 * const Capitalized = Schema.String.pipe(
667 * Schema.decode(SchemaTransformation.capitalize())
668 * )
669 * Schema.decodeSync(Capitalized)("hello") // => "Hello"
670 * ```
671 *
672 * @see {@link uncapitalize}
673 * @see {@link toUpperCase}
674 *
675 * @category transforming
676 * @since 4.0.0
677 */
678export function capitalize(): Transformation<string, string> {
679 return new Transformation(
680 SchemaGetter.capitalize(),
681 SchemaGetter.passthrough()
682 )
683}
684
685/**
686 * Transforms strings by lowercasing the first character on
687 * decode. Encode is passthrough.
688 *
689 * **When to use**
690 *
691 * Use when you need a schema transformation to normalize identifiers or field
692 * names.
693 *
694 * **Details**
695 *
696 * Decoding lowercases the first character and leaves the rest unchanged.
697 * Encoding is passthrough.
698 *
699 * **Example** (Uncapitalizing on decode)
700 *
701 * ```ts import.meta.vitest
702 * import { Schema, SchemaTransformation } from "effect"
703 *
704 * const Uncapitalized = Schema.String.pipe(
705 * Schema.decode(SchemaTransformation.uncapitalize())
706 * )
707 * Schema.decodeSync(Uncapitalized)("Hello") // => "hello"
708 * ```
709 *
710 * @see {@link capitalize}
711 * @see {@link toLowerCase}
712 *
713 * @category transforming
714 * @since 4.0.0
715 */
716export function uncapitalize(): Transformation<string, string> {
717 return new Transformation(
718 SchemaGetter.uncapitalize(),
719 SchemaGetter.passthrough()
720 )
721}
722
723/**
724 * Transforms a string into a record of key-value pairs and
725 * encodes a record of key-value pairs into a string.
726 *
727 * **When to use**
728 *
729 * Use when you need a schema transformation to parse query-string-like or
730 * config-file-like strings into records.
731 *
732 * **Details**
733 *
734 * Decoding splits the string by `separator` (default `","`) into pairs, then
735 * splits each pair by `keyValueSeparator` (default `"="`). Encoding joins the
736 * record back into a string using the same separators. The transformation is
737 * round-trippable when keys and values do not contain the separators.
738 *
739 * **Example** (Parsing key-value pairs)
740 *
741 * ```ts import.meta.vitest
742 * import { Schema, SchemaTransformation } from "effect"
743 *
744 * const Config = Schema.String.pipe(
745 * Schema.decodeTo(
746 * Schema.Record(Schema.String, Schema.String),
747 * SchemaTransformation.splitKeyValue({ separator: ";", keyValueSeparator: ":" })
748 * )
749 * )
750 * Schema.decodeSync(Config)("host:localhost;port:3000") // => { host: "localhost", port: "3000" }
751 * ```
752 *
753 * @see {@link trim}
754 * @see {@link snakeToCamel}
755 *
756 * @category transforming
757 * @since 4.0.0
758 */
759export function splitKeyValue(options?: {
760 readonly separator?: string | undefined
761 readonly keyValueSeparator?: string | undefined
762}): Transformation<Record<string, string>, string> {
763 return new Transformation(
764 SchemaGetter.splitKeyValue(options),
765 SchemaGetter.joinKeyValue(options)
766 )
767}
768
769const passthrough_ = new Transformation(
770 SchemaGetter.passthrough<any>(),
771 SchemaGetter.passthrough<any>()
772)
773
774/**
775 * Transforms values by returning the input unchanged in both
776 * directions.
777 *
778 * **When to use**
779 *
780 * Use when you need a schema transformation to connect two schemas that share
781 * the same type with no actual conversion.
782 *
783 * **Details**
784 *
785 * - Both decode and encode are no-ops.
786 * - Returns a shared singleton instance (no allocation per call).
787 * - By default, `T` and `E` must be the same type. Pass `{ strict: false }`
788 * to bypass the type constraint.
789 *
790 * **Example** (Chaining schemas with no conversion)
791 *
792 * ```ts import.meta.vitest
793 * import { Schema, SchemaTransformation } from "effect"
794 *
795 * const schema = Schema.Trim.pipe(
796 * Schema.decodeTo(Schema.FiniteFromString, SchemaTransformation.passthrough())
797 * )
798 * Schema.decodeSync(schema)("1") // => 1
799 * ```
800 *
801 * @see {@link passthroughSupertype}
802 * @see {@link passthroughSubtype}
803 * @see {@link transform}
804 *
805 * @category constructors
806 * @since 4.0.0
807 */
808export function passthrough<T, E>(options: { readonly strict: false }): Transformation<T, E>
809export function passthrough<T>(): Transformation<T, T>
810export function passthrough<T>(): Transformation<T, T> {
811 return passthrough_
812}
813
814/**
815 * Transforms values without changing them, typed so that `T extends E`, where the decoded
816 * type `T` is a subtype of the encoded type `E`.
817 *
818 * **When to use**
819 *
820 * Use when you need a no-op schema transformation whose decoded side is
821 * narrower than the encoded side.
822 *
823 * **Details**
824 *
825 * Both decode and encode are no-ops and return a shared singleton
826 * transformation.
827 *
828 * **Example** (Passing through supertypes)
829 *
830 * ```ts import.meta.vitest
831 * import { SchemaTransformation } from "effect"
832 *
833 * const t: SchemaTransformation.Transformation<"a" | "b", string> =
834 * SchemaTransformation.passthroughSupertype<"a" | "b", string>()
835 * ```
836 *
837 * @see {@link passthrough}
838 * @see {@link passthroughSubtype}
839 *
840 * @category constructors
841 * @since 4.0.0
842 */
843export function passthroughSupertype<T extends E, E>(): Transformation<T, E>
844export function passthroughSupertype<T>(): Transformation<T, T> {
845 return passthrough_
846}
847
848/**
849 * Transforms values without changing them, typed so that `E extends T` — the encoded
850 * type is a subtype of the decoded type.
851 *
852 * **When to use**
853 *
854 * Use when you need a no-op schema transformation whose encoded side is more
855 * specific than its decoded side.
856 *
857 * **Details**
858 *
859 * - Both decode and encode are no-ops (same as {@link passthrough}).
860 * - Returns a shared singleton instance.
861 *
862 * **Example** (Passing through subtypes)
863 *
864 * ```ts import.meta.vitest
865 * import { SchemaTransformation } from "effect"
866 *
867 * const t: SchemaTransformation.Transformation<string, "a" | "b"> =
868 * SchemaTransformation.passthroughSubtype<string, "a" | "b">()
869 * ```
870 *
871 * @see {@link passthrough}
872 * @see {@link passthroughSupertype}
873 *
874 * @category constructors
875 * @since 4.0.0
876 */
877export function passthroughSubtype<T, E extends T>(): Transformation<T, E>
878export function passthroughSubtype<T>(): Transformation<T, T> {
879 return passthrough_
880}
881
882/**
883 * Decodes a `string` into a `number` and encodes a `number` back to a
884 * `string`.
885 *
886 * **When to use**
887 *
888 * Use when you need a schema transformation to parse numeric strings from APIs,
889 * form data, or URL parameters.
890 *
891 * **Details**
892 *
893 * Decoding coerces the string to a number like `Number(s)`. Encoding coerces
894 * the number to a string like `String(n)`. This does not validate that the
895 * result is finite; combine with `Schema.Finite` or `Schema.Int` for stricter
896 * checks.
897 *
898 * **Example** (Converting a string to a number)
899 *
900 * ```ts import.meta.vitest
901 * import { Schema, SchemaTransformation } from "effect"
902 *
903 * const schema = Schema.String.pipe(
904 * Schema.decodeTo(Schema.Number, SchemaTransformation.numberFromString)
905 * )
906 * Schema.decodeSync(schema)("42") // => 42
907 * ```
908 *
909 * @see {@link bigintFromString}
910 * @see {@link transform}
911 *
912 * @category converting
913 * @since 4.0.0
914 */
915export const numberFromString = new Transformation(
916 SchemaGetter.Number(),
917 SchemaGetter.String()
918)
919
920/**
921 * Decodes a `string` into a `bigint` and encodes a `bigint` back to a
922 * `string`.
923 *
924 * **When to use**
925 *
926 * Use when you need a schema transformation to parse large integer strings
927 * (e.g. database IDs, blockchain values).
928 *
929 * **Details**
930 *
931 * Decoding coerces the string to a bigint like `BigInt(s)`. Encoding coerces
932 * the bigint to a string like `String(n)`. Decoding fails if the string is not
933 * a valid bigint representation.
934 *
935 * **Example** (Converting a string to a BigInt)
936 *
937 * ```ts import.meta.vitest
938 * import { Schema, SchemaTransformation } from "effect"
939 *
940 * const schema = Schema.String.pipe(
941 * Schema.decodeTo(Schema.BigInt, SchemaTransformation.bigintFromString)
942 * )
943 * Schema.decodeSync(schema)("42") // => 42n
944 * ```
945 *
946 * @see {@link numberFromString}
947 * @see {@link transform}
948 *
949 * @category converting
950 * @since 4.0.0
951 */
952export const bigintFromString = new Transformation(
953 SchemaGetter.BigInt(),
954 SchemaGetter.String()
955)
956
957/**
958 * Decodes a `string` into a `Date` and encodes a `Date` back to a `string`.
959 *
960 * **When to use**
961 *
962 * Use when you need a schema transformation to parse date strings from APIs or
963 * user input.
964 *
965 * **Details**
966 *
967 * Decoding creates a `Date` from the string like `new Date(s)`. Encoding
968 * converts the `Date` to an ISO string like `date.toISOString()`, returning
969 * `"Invalid Date"` for invalid dates.
970 *
971 * **Example** (Converting a string to a Date)
972 *
973 * ```ts import.meta.vitest
974 * import { Schema, SchemaTransformation } from "effect"
975 *
976 * const schema = Schema.String.pipe(
977 * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString)
978 * )
979 * Schema.decodeSync(schema)("2024-01-01").toISOString() // => "2024-01-01T00:00:00.000Z"
980 * ```
981 *
982 * @see {@link dateFromMillis}
983 * @see {@link dateTimeUtcFromString}
984 *
985 * @category converting
986 * @since 4.0.0
987 */
988export const dateFromString: Transformation<globalThis.Date, string> = new Transformation(
989 SchemaGetter.Date(),
990 SchemaGetter.transform(formatDate)
991)
992
993/**
994 * Decodes epoch milliseconds into a `Date` and encodes a `Date` back to epoch
995 * milliseconds.
996 *
997 * **When to use**
998 *
999 * Use when you need a schema transformation for numeric timestamps represented
1000 * as milliseconds since the Unix epoch.
1001 *
1002 * **Details**
1003 *
1004 * Decoding creates a `Date` from the number like `new Date(ms)`. Encoding
1005 * returns the `Date` timestamp like `date.getTime()`.
1006 *
1007 * **Gotchas**
1008 *
1009 * This transformation does not validate date validity. `NaN`, `Infinity`, and
1010 * `-Infinity` decode to invalid `Date` instances.
1011 *
1012 * **Example** (Converting milliseconds to a Date)
1013 *
1014 * ```ts import.meta.vitest
1015 * import { Schema, SchemaTransformation } from "effect"
1016 *
1017 * const schema = Schema.Number.pipe(
1018 * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromMillis)
1019 * )
1020 * Schema.decodeSync(schema)(0).toISOString() // => "1970-01-01T00:00:00.000Z"
1021 * ```
1022 *
1023 * @see {@link dateFromString}
1024 * @see {@link SchemaGetter.dateTimeUtcFromInput}
1025 *
1026 * @category converting
1027 * @since 4.0.0
1028 */
1029export const dateFromMillis: Transformation<globalThis.Date, number> = new Transformation(
1030 SchemaGetter.Date(),
1031 SchemaGetter.transform((date) => date.getTime())
1032)
1033
1034/**
1035 * Decodes a `string` into a `Duration` and encodes a `Duration` back to a
1036 * parseable `string`.
1037 *
1038 * **When to use**
1039 *
1040 * Use when you need a schema transformation to parse human-readable duration
1041 * strings from APIs, config, or user input.
1042 *
1043 * **Details**
1044 *
1045 * Decoding accepts any string that `Duration.fromInput` can parse, including
1046 * `"Infinity"` and `"-Infinity"`. Encoding returns `String(duration)`,
1047 * producing strings such as `"2000 millis"` or `"10 nanos"` that round-trip
1048 * through the parser.
1049 *
1050 * **Example** (Converting a string to a Duration)
1051 *
1052 * ```ts import.meta.vitest
1053 * import { Schema, SchemaTransformation } from "effect"
1054 *
1055 * const schema = Schema.String.pipe(
1056 * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromString)
1057 * )
1058 * String(Schema.decodeSync(schema)("5 seconds")) // => "5000 millis"
1059 * ```
1060 *
1061 * @see {@link durationFromNanos}
1062 * @see {@link durationFromMillis}
1063 *
1064 * @category transforming
1065 * @since 4.0.0
1066 */
1067export const durationFromString: Transformation<Duration.Duration, string> = transformEffect<
1068 Duration.Duration,
1069 string
1070>({
1071 decode: (s, options) =>
1072 Option.match(Duration.fromInput(s as Duration.Input), {
1073 onNone: () =>
1074 Effect.fail(
1075 new SchemaIssue.InvalidValue(
1076 { expected: "a valid Duration string" },
1077 s,
1078 options
1079 )
1080 ),
1081 onSome: Effect.succeed
1082 }),
1083 encode: (duration) => Effect.succeed(String(duration))
1084})
1085
1086/**
1087 * Decodes a `bigint` (nanoseconds) into a `Duration` and encodes a
1088 * `Duration` back to `bigint` nanoseconds.
1089 *
1090 * **When to use**
1091 *
1092 * Use when you need a schema transformation for nanosecond-precision timestamps
1093 * or intervals.
1094 *
1095 * **Details**
1096 *
1097 * Decoding always succeeds and creates a `Duration` from nanoseconds. Encoding
1098 * fails with `InvalidValue` if the `Duration` cannot be represented as a
1099 * `bigint`, such as `Duration.infinity`.
1100 *
1101 * **Example** (Converting nanoseconds to a Duration)
1102 *
1103 * ```ts import.meta.vitest
1104 * import { Schema, SchemaTransformation } from "effect"
1105 *
1106 * const schema = Schema.BigInt.pipe(
1107 * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromNanos)
1108 * )
1109 * String(Schema.decodeSync(schema)(5n)) // => "5 nanos"
1110 * ```
1111 *
1112 * @see {@link durationFromMillis}
1113 *
1114 * @category transforming
1115 * @since 4.0.0
1116 */
1117export const durationFromNanos: Transformation<Duration.Duration, bigint> = transformEffect({
1118 decode: (i) => Effect.succeed(Duration.nanos(i)),
1119 encode: (a, options) =>
1120 Option.match(Duration.toNanos(a), {
1121 onNone: () =>
1122 Effect.fail(
1123 new SchemaIssue.InvalidValue(
1124 { expected: "a Duration representable as a bigint" },
1125 a,
1126 options
1127 )
1128 ),
1129 onSome: (nanos) => Effect.succeed(nanos)
1130 })
1131})
1132
1133/**
1134 * Decodes a `number` of milliseconds into a `Duration` and encodes a `Duration`
1135 * back to milliseconds.
1136 *
1137 * **When to use**
1138 *
1139 * Use when you need a schema transformation to decode timeouts, delays, elapsed
1140 * intervals, or other duration values stored as millisecond counts.
1141 *
1142 * **Details**
1143 *
1144 * Decode creates a duration from the number, and encode returns the duration
1145 * length in milliseconds.
1146 *
1147 * **Example** (Converting milliseconds to a Duration)
1148 *
1149 * ```ts import.meta.vitest
1150 * import { Schema, SchemaTransformation } from "effect"
1151 *
1152 * const schema = Schema.Number.pipe(
1153 * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromMillis)
1154 * )
1155 * String(Schema.decodeSync(schema)(5000)) // => "5000 millis"
1156 * ```
1157 *
1158 * @see {@link durationFromNanos}
1159 *
1160 * @category transforming
1161 * @since 4.0.0
1162 */
1163export const durationFromMillis: Transformation<Duration.Duration, number> = transform({
1164 decode: (i) => Duration.millis(i),
1165 encode: (a) => Duration.toMillis(a)
1166})
1167
1168/**
1169 * Decodes a string into a `ByteSize` and encodes it as an exact string.
1170 *
1171 * @category transforming
1172 * @since 4.0.0
1173 */
1174export const byteSizeFromString: Transformation<ByteSize.ByteSize, string> = transformEffect({
1175 decode: (input, options) =>
1176 Option.match(ByteSize.fromString(input), {
1177 onNone: () =>
1178 Effect.fail(
1179 new SchemaIssue.InvalidValue(
1180 { expected: "a valid ByteSize string" },
1181 input,
1182 options
1183 )
1184 ),
1185 onSome: Effect.succeed
1186 }),
1187 encode: (byteSize) => Effect.succeed(`${byteSize} ${byteSize === BigInt(1) ? "byte" : "bytes"}`)
1188})
1189
1190/**
1191 * Decodes a non-negative bigint byte count into a `ByteSize`.
1192 *
1193 * @category transforming
1194 * @since 4.0.0
1195 */
1196export const byteSizeFromBigInt: Transformation<ByteSize.ByteSize, bigint> = transformEffect({
1197 decode: (input, options) =>
1198 Option.match(ByteSize.fromInput(input), {
1199 onNone: () =>
1200 Effect.fail(
1201 new SchemaIssue.InvalidValue(
1202 { expected: "a non-negative bigint byte count" },
1203 input,
1204 options
1205 )
1206 ),
1207 onSome: Effect.succeed
1208 }),
1209 encode: (byteSize) => Effect.succeed(ByteSize.toBigInt(byteSize))
1210})
1211
1212/**
1213 * Decodes a non-negative safe-integer byte count into a `ByteSize`.
1214 *
1215 * @category transforming
1216 * @since 4.0.0
1217 */
1218export const byteSizeFromNumber: Transformation<ByteSize.ByteSize, number> = transformEffect({
1219 decode: (input, options) =>
1220 Option.match(ByteSize.fromInput(input), {
1221 onNone: () =>
1222 Effect.fail(
1223 new SchemaIssue.InvalidValue(
1224 { expected: "a non-negative safe-integer byte count" },
1225 input,
1226 options
1227 )
1228 ),
1229 onSome: Effect.succeed
1230 }),
1231 encode: (byteSize, options) =>
1232 Option.match(ByteSize.toNumber(byteSize), {
1233 onNone: () =>
1234 Effect.fail(
1235 new SchemaIssue.InvalidValue(
1236 { expected: "a ByteSize representable as a safe integer" },
1237 byteSize,
1238 options
1239 )
1240 ),
1241 onSome: Effect.succeed
1242 })
1243})
1244
1245type JsonError = {
1246 message: string
1247 name?: string
1248 stack?: string
1249 cause?: Json
1250}
1251
1252const isJsonError = (input: unknown): input is JsonError =>
1253 Predicate.isObject(input) && typeof input["message"] === "string"
1254
1255const decodeJsonError = (input: JsonError): Error => {
1256 const hasCause = Object.hasOwn(input, "cause")
1257 const err = hasCause
1258 ? new Error(input.message, { cause: decodeDefect(input.cause as Json) })
1259 : new Error(input.message)
1260 if (typeof input.name === "string" && input.name !== "Error") err.name = input.name
1261 if (typeof input.stack === "string") err.stack = input.stack
1262 return err
1263}
1264
1265const encodeUnknownAsJson = (input: unknown): Json => {
1266 try {
1267 const json = formatJson(input)
1268 return json === undefined ? format(input) : JSON.parse(json)
1269 } catch {
1270 return format(input)
1271 }
1272}
1273
1274const encodeJsonError = (
1275 input: Error,
1276 options: ErrorOptions | undefined,
1277 encodeDefect: (input: unknown) => Json
1278): JsonError => {
1279 const encoded: JsonError = {
1280 name: input.name,
1281 message: typeof input.message === "string" ? input.message : ""
1282 }
1283 if (options?.includeStack && typeof input.stack === "string") {
1284 encoded.stack = input.stack
1285 }
1286 if (!options?.excludeCause && input.cause !== undefined) {
1287 encoded.cause = encodeDefect(input.cause)
1288 }
1289 return encoded
1290}
1291
1292const makeEncodeDefect = (options?: ErrorOptions): (input: unknown) => Json => {
1293 const seen = new WeakSet<object>()
1294 const encode = (input: unknown): Json => {
1295 if (Predicate.isError(input)) {
1296 if (seen.has(input)) {
1297 return "[Circular]"
1298 }
1299 seen.add(input)
1300 const encoded = encodeJsonError(input, options, encode)
1301 seen.delete(input)
1302 return encoded
1303 }
1304 return encodeUnknownAsJson(input)
1305 }
1306 return encode
1307}
1308
1309const decodeDefect = (input: Json): unknown => isJsonError(input) ? decodeJsonError(input) : input
1310
1311/** @internal */
1312export const errorFromJsonError = (options?: ErrorOptions): Transformation<Error, JsonError> =>
1313 transform({
1314 decode: decodeJsonError,
1315 encode: (input) => makeEncodeDefect(options)(input) as JsonError
1316 })
1317
1318/** @internal */
1319export const defectFromJson = (options?: ErrorOptions) =>
1320 transform({
1321 decode: decodeDefect,
1322 encode: makeEncodeDefect(options)
1323 })
1324
1325/**
1326 * Decodes `T | null` into `Option<T>` and encodes `Option<T>` back to
1327 * `T | null`.
1328 *
1329 * **When to use**
1330 *
1331 * Use when you need a schema transformation to convert nullable API fields to
1332 * `Option`.
1333 *
1334 * **Details**
1335 *
1336 * Decoding maps `null` to `Option.none()` and non-null values to
1337 * `Option.some(value)`. Encoding maps `Option.none()` to `null` and
1338 * `Option.some(value)` to `value`. The transformation is pure and synchronous.
1339 *
1340 * **Example** (Converting nullable values to an Option)
1341 *
1342 * ```ts import.meta.vitest
1343 * import { Option, Schema, SchemaTransformation } from "effect"
1344 *
1345 * const schema = Schema.NullOr(Schema.String).pipe(
1346 * Schema.decodeTo(
1347 * Schema.Option(Schema.String),
1348 * SchemaTransformation.optionFromNullOr()
1349 * )
1350 * )
1351 * Schema.decodeSync(schema)(null) // => Option.none()
1352 * ```
1353 *
1354 * @see {@link optionFromNullishOr}
1355 *
1356 * @category transforming
1357 * @since 4.0.0
1358 */
1359export function optionFromNullOr<T>(): Transformation<Option.Option<T>, T | null> {
1360 return transform({
1361 decode: Option.fromNullOr,
1362 encode: Option.getOrNull
1363 })
1364}
1365
1366/**
1367 * Decodes `T | undefined` into `Option<T>` and encodes `Option.none()` back to
1368 * `undefined`.
1369 *
1370 * **When to use**
1371 *
1372 * Use when you need a schema transformation to convert API fields that use
1373 * `undefined` for absence to `Option`.
1374 *
1375 * **Details**
1376 *
1377 * Decoding maps `undefined` to `Option.none()` and non-undefined values to
1378 * `Option.some(value)`. Encoding maps `Option.none()` to `undefined` and
1379 * `Option.some(value)` to `value`. The transformation is pure and synchronous.
1380 *
1381 * **Example** (Converting undefined-or values to an Option)
1382 *
1383 * ```ts import.meta.vitest
1384 * import { Option, Schema, SchemaTransformation } from "effect"
1385 *
1386 * const schema = Schema.UndefinedOr(Schema.String).pipe(
1387 * Schema.decodeTo(
1388 * Schema.Option(Schema.String),
1389 * SchemaTransformation.optionFromUndefinedOr()
1390 * )
1391 * )
1392 * Schema.decodeSync(schema)(undefined) // => Option.none()
1393 * ```
1394 *
1395 * @see {@link optionFromOptionalKey}
1396 * @see {@link optionFromOptional}
1397 *
1398 * @category transforming
1399 * @since 4.0.0
1400 */
1401export function optionFromUndefinedOr<T>(): Transformation<Option.Option<T>, T | undefined> {
1402 return transform({
1403 decode: Option.fromUndefinedOr,
1404 encode: Option.getOrUndefined
1405 })
1406}
1407
1408/**
1409 * Decodes `T | null | undefined` into `Option<T>` and encodes `Option<T>`
1410 * back to `T | null` or `T | undefined` depending on the provided
1411 * `options.onNoneEncoding` (defaults to `undefined`).
1412 *
1413 * **When to use**
1414 *
1415 * Use when you need a schema transformation to convert nullish API fields to
1416 * `Option` when both `null` and `undefined` represent absence.
1417 *
1418 * **Details**
1419 *
1420 * Decoding maps `null` and `undefined` to `Option.none()` and all other values
1421 * to `Option.some(value)`. Encoding maps `Option.none()` to `null` or
1422 * `undefined` according to `options.onNoneEncoding`, and maps
1423 * `Option.some(value)` to `value`. The transformation is pure and synchronous.
1424 *
1425 * **Example** (Converting nullish values to an Option and encoding None as null)
1426 *
1427 * ```ts import.meta.vitest
1428 * import { Option, Schema, SchemaTransformation } from "effect"
1429 *
1430 * const schema = Schema.NullishOr(Schema.String).pipe(
1431 * Schema.decodeTo(
1432 * Schema.Option(Schema.String),
1433 * SchemaTransformation.optionFromNullishOr({ onNoneEncoding: null })
1434 * )
1435 * )
1436 * Schema.encodeSync(schema)(Option.none()) // => null
1437 * ```
1438 *
1439 * @see {@link optionFromNullOr}
1440 * @see {@link optionFromUndefinedOr}
1441 *
1442 * @category transforming
1443 * @since 4.0.0
1444 */
1445export function optionFromNullishOr<T>(
1446 options?: {
1447 onNoneEncoding: null | undefined
1448 }
1449): Transformation<Option.Option<T>, T | null | undefined> {
1450 return transform({
1451 decode: Option.fromNullishOr,
1452 encode: options?.onNoneEncoding === null ? Option.getOrNull : Option.getOrUndefined
1453 })
1454}
1455
1456/**
1457 * Decodes an optional struct key into `Option<T>` and encodes `Option<T>`
1458 * back to an optional key.
1459 *
1460 * **When to use**
1461 *
1462 * Use when you need a schema transformation to convert optional struct keys
1463 * (declared with `Schema.optionalKey`) to `Option` values.
1464 *
1465 * **Details**
1466 *
1467 * Decoding maps an absent key (`None`) to `Some(None)` and a present key
1468 * (`Some(v)`) to `Some(Some(v))`. Encoding maps `Some(None)` to `None` to omit
1469 * the key, and maps `Some(Some(v))` to `Some(v)`. This uses
1470 * `transformOptional` under the hood.
1471 *
1472 * **Example** (Converting an optional key to an Option)
1473 *
1474 * ```ts import.meta.vitest
1475 * import { Option, Schema, SchemaTransformation } from "effect"
1476 *
1477 * const schema = Schema.Struct({
1478 * name: Schema.optionalKey(Schema.String).pipe(
1479 * Schema.decodeTo(
1480 * Schema.Option(Schema.String),
1481 * SchemaTransformation.optionFromOptionalKey()
1482 * )
1483 * )
1484 * })
1485 * Schema.decodeSync(schema)({}).name // => Option.none()
1486 * ```
1487 *
1488 * @see {@link optionFromOptional}
1489 * @see {@link optionFromUndefinedOr}
1490 * @see {@link transformOptional}
1491 *
1492 * @category transforming
1493 * @since 4.0.0
1494 */
1495export function optionFromOptionalKey<T>(): Transformation<Option.Option<T>, T> {
1496 return transformOptional({
1497 decode: Option.some,
1498 encode: Option.flatten
1499 })
1500}
1501
1502/**
1503 * Decodes optional values into `Option<T>` and encodes `Option.none()` back to
1504 * an omitted optional value.
1505 *
1506 * **When to use**
1507 *
1508 * Use when you need a schema transformation to convert optional (possibly
1509 * `undefined`) values to `Option`.
1510 *
1511 * **Details**
1512 *
1513 * Decoding maps an absent or `undefined` value to `Some(None)` and a present
1514 * value to `Some(Some(v))`. Encoding maps `Some(None)` to `None` to omit the
1515 * value, and maps `Some(Some(v))` to `Some(v)`. This uses
1516 * `transformOptional` under the hood and filters out `undefined` on decode.
1517 *
1518 * **Example** (Converting an optional value to an Option)
1519 *
1520 * ```ts import.meta.vitest
1521 * import { Option, Schema, SchemaTransformation } from "effect"
1522 *
1523 * const schema = Schema.Struct({
1524 * age: Schema.optional(Schema.Number).pipe(
1525 * Schema.decodeTo(
1526 * Schema.Option(Schema.Number),
1527 * SchemaTransformation.optionFromOptional()
1528 * )
1529 * )
1530 * })
1531 * Schema.decodeSync(schema)({ age: undefined }).age // => Option.none()
1532 * ```
1533 *
1534 * @see {@link optionFromOptionalKey}
1535 * @see {@link optionFromUndefinedOr}
1536 * @see {@link transformOptional}
1537 *
1538 * @category transforming
1539 * @since 4.0.0
1540 */
1541export function optionFromOptional<T>(): Transformation<Option.Option<T>, T | undefined> {
1542 return transformOptional<Option.Option<T>, T | undefined>({
1543 decode: (ot) => ot.pipe(Option.filter(Predicate.isNotUndefined), Option.some),
1544 encode: Option.flatten
1545 })
1546}
1547
1548/**
1549 * Decodes a `string` into a `URL` and encodes a `URL` back to its `href`
1550 * string.
1551 *
1552 * **When to use**
1553 *
1554 * Use when you need a schema transformation to parse URL strings from user
1555 * input or API responses.
1556 *
1557 * **Details**
1558 *
1559 * Decoding checks `URL.canParse(s)` and fails with `InvalidValue` if the string
1560 * is not a valid URL. Encoding returns `url.href`.
1561 *
1562 * **Example** (Converting a string to a URL)
1563 *
1564 * ```ts import.meta.vitest
1565 * import { Schema, SchemaTransformation } from "effect"
1566 *
1567 * const schema = Schema.String.pipe(
1568 * Schema.decodeTo(Schema.URL, SchemaTransformation.urlFromString)
1569 * )
1570 * Schema.decodeSync(schema)("https://example.com/path").href // => "https://example.com/path"
1571 * ```
1572 *
1573 * @see {@link numberFromString}
1574 * @see {@link transformEffect}
1575 *
1576 * @category transforming
1577 * @since 4.0.0
1578 */
1579export const urlFromString: Transformation<URL, string> = transformEffect<URL, string>({
1580 decode: (s, options) =>
1581 URL.canParse(s)
1582 ? Effect.succeed(new URL(s))
1583 : Effect.fail(
1584 new SchemaIssue.InvalidValue(
1585 { expected: "a valid URL string" },
1586 s,
1587 options
1588 )
1589 ),
1590 encode: (url) => Effect.succeed(url.href)
1591})
1592
1593/**
1594 * Decodes a `string` into a `BigDecimal` and encodes a `BigDecimal` back to
1595 * its string representation.
1596 *
1597 * **When to use**
1598 *
1599 * Use when you need a schema transformation to parse decimal number strings
1600 * from APIs or user input.
1601 *
1602 * **Details**
1603 *
1604 * Decoding calls `BigDecimal.fromString(s)` and fails with `InvalidValue` if
1605 * the string is not a valid `BigDecimal` representation. Encoding returns
1606 * `BigDecimal.format(bd)`.
1607 *
1608 * @category transforming
1609 * @since 4.0.0
1610 */
1611export const bigDecimalFromString: Transformation<BigDecimal.BigDecimal, string> = transformEffect<
1612 BigDecimal.BigDecimal,
1613 string
1614>({
1615 decode: (s, options) => {
1616 const result = BigDecimal.fromString(s)
1617 return Option.isNone(result)
1618 ? Effect.fail(
1619 new SchemaIssue.InvalidValue(
1620 { expected: "a valid BigDecimal string" },
1621 s,
1622 options
1623 )
1624 )
1625 : Effect.succeed(result.value)
1626 },
1627 encode: (bd) => Effect.succeed(BigDecimal.format(bd))
1628})
1629
1630/**
1631 * Decodes a Base64-encoded `string` into a `Uint8Array` and encodes a
1632 * `Uint8Array` back to a Base64 string.
1633 *
1634 * **When to use**
1635 *
1636 * Use when you need a schema transformation for binary data transmitted as
1637 * Base64 strings (e.g. file uploads, API payloads).
1638 *
1639 * **Details**
1640 *
1641 * Decoding parses the Base64 string into bytes. Encoding writes the byte array
1642 * as a Base64 string.
1643 *
1644 * **Example** (Converting Base64 to a Uint8Array)
1645 *
1646 * ```ts import.meta.vitest
1647 * import { Schema, SchemaTransformation } from "effect"
1648 *
1649 * const schema = Schema.String.pipe(
1650 * Schema.decodeTo(Schema.Uint8Array, SchemaTransformation.uint8ArrayFromBase64String)
1651 * )
1652 * Array.from(Schema.decodeSync(schema)("AQID")) // => [1, 2, 3]
1653 * ```
1654 *
1655 * @see {@link fromJsonString}
1656 * @see `Schema.Uint8ArrayFromBase64` - a ready-made schema wrapping this transformation.
1657 *
1658 * @category encoding
1659 * @since 4.0.0
1660 */
1661export const uint8ArrayFromBase64String: Transformation<Uint8Array<ArrayBufferLike>, string> = new Transformation(
1662 SchemaGetter.decodeBase64(),
1663 SchemaGetter.encodeBase64()
1664)
1665
1666/**
1667 * Decodes a Base64-encoded `string` into a UTF-8 `string` and encodes a
1668 * UTF-8 `string` back to a Base64 string.
1669 *
1670 * **When to use**
1671 *
1672 * Use when you need a schema transformation for text data transmitted as Base64
1673 * strings.
1674 *
1675 * **Details**
1676 *
1677 * Decoding parses the Base64 string into a UTF-8 string. Encoding writes the
1678 * string as a Base64 string.
1679 *
1680 * **Example** (Converting Base64 to a string)
1681 *
1682 * ```ts import.meta.vitest
1683 * import { Schema, SchemaTransformation } from "effect"
1684 *
1685 * const schema = Schema.String.pipe(
1686 * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromBase64String)
1687 * )
1688 * Schema.decodeSync(schema)("aGVsbG8=") // => "hello"
1689 * ```
1690 *
1691 * @see {@link uint8ArrayFromBase64String}
1692 * @see `Schema.StringFromBase64` - a ready-made schema wrapping this transformation.
1693 *
1694 * @category encoding
1695 * @since 4.0.0
1696 */
1697export const stringFromBase64String: Transformation<string, string> = new Transformation(
1698 SchemaGetter.decodeBase64String(),
1699 SchemaGetter.encodeBase64()
1700)
1701
1702/**
1703 * Decodes a base64 (URL) encoded `string` into a UTF-8 `string` and encodes it back.
1704 *
1705 * **When to use**
1706 *
1707 * Use when you need a schema transformation for text data transmitted as Base64
1708 * URL-safe strings.
1709 *
1710 * **Details**
1711 *
1712 * Decoding parses the Base64 URL string into a UTF-8 string. Encoding writes
1713 * the string as a Base64 URL string.
1714 *
1715 * **Example** (Converting Base64Url to a string)
1716 *
1717 * ```ts import.meta.vitest
1718 * import { Schema, SchemaTransformation } from "effect"
1719 *
1720 * const schema = Schema.String.pipe(
1721 * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromBase64UrlString)
1722 * )
1723 * Schema.decodeSync(schema)("aGVsbG8") // => "hello"
1724 * ```
1725 *
1726 * @see {@link stringFromBase64String}
1727 * @see `Schema.StringFromBase64Url` - a ready-made schema wrapping this transformation.
1728 *
1729 * @category encoding
1730 * @since 4.0.0
1731 */
1732export const stringFromBase64UrlString: Transformation<string, string> = new Transformation(
1733 SchemaGetter.decodeBase64UrlString(),
1734 SchemaGetter.encodeBase64Url()
1735)
1736
1737/**
1738 * Decodes a hex encoded `string` into a UTF-8 `string` and encodes it back.
1739 *
1740 * **When to use**
1741 *
1742 * Use when you need a schema transformation for text data transmitted as
1743 * hexadecimal strings.
1744 *
1745 * **Details**
1746 *
1747 * Decoding parses the hex string into a UTF-8 string. Encoding writes the
1748 * string as a hex string.
1749 *
1750 * **Example** (Converting hex to a string)
1751 *
1752 * ```ts import.meta.vitest
1753 * import { Schema, SchemaTransformation } from "effect"
1754 *
1755 * const schema = Schema.String.pipe(
1756 * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromHexString)
1757 * )
1758 * Schema.decodeSync(schema)("68656c6c6f") // => "hello"
1759 * ```
1760 *
1761 * @see {@link stringFromBase64String}
1762 * @see `Schema.StringFromHex` - a ready-made schema wrapping this transformation.
1763 *
1764 * @category encoding
1765 * @since 4.0.0
1766 */
1767export const stringFromHexString: Transformation<string, string> = new Transformation(
1768 SchemaGetter.decodeHexString(),
1769 SchemaGetter.encodeHex()
1770)
1771
1772/**
1773 * Decodes a URI component encoded string into a UTF-8 string and encodes a
1774 * UTF-8 string into a URI component encoded string.
1775 *
1776 * **When to use**
1777 *
1778 * Use when you need a schema transformation to store structured data in URL
1779 * query parameters or fragments, such as composing with `Schema.parseJson` to
1780 * round-trip JSON through a URL.
1781 *
1782 * **Details**
1783 *
1784 * Decoding calls `decodeURIComponent` and fails if the input contains malformed
1785 * percent-encoding sequences. Encoding calls `encodeURIComponent`.
1786 *
1787 * **Example** (Defining a URI component schema)
1788 *
1789 * ```ts import.meta.vitest
1790 * import { Schema, SchemaTransformation } from "effect"
1791 *
1792 * const schema = Schema.String.pipe(
1793 * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromUriComponent)
1794 * )
1795 * Schema.decodeSync(schema)("hello%20world") // => "hello world"
1796 * ```
1797 *
1798 * @see {@link stringFromBase64String}
1799 * @see `Schema.StringFromUriComponent` - a ready-made schema wrapping this transformation.
1800 *
1801 * @category encoding
1802 * @since 4.0.0
1803 */
1804export const stringFromUriComponent: Transformation<string, string> = new Transformation(
1805 SchemaGetter.decodeUriComponent(),
1806 SchemaGetter.encodeUriComponent()
1807)
1808
1809/**
1810 * Decodes a JSON string with `JSON.parse` and encodes a value with
1811 * `JSON.stringify`.
1812 *
1813 * **When to use**
1814 *
1815 * Use when you need a schema transformation to decode JSON stored or
1816 * transmitted as a string, usually before composing with another schema that
1817 * validates the parsed structure.
1818 *
1819 * **Details**
1820 *
1821 * The `reviver` option is passed to `JSON.parse` during decoding. The
1822 * `replacer` and `space` options are passed to `JSON.stringify` during
1823 * encoding. Decode fails with `InvalidValue` for invalid JSON, and encode can
1824 * fail with `InvalidValue` when `JSON.stringify` cannot serialize the value.
1825 *
1826 * **Example** (Parsing JSON)
1827 *
1828 * ```ts import.meta.vitest
1829 * import { Schema, SchemaTransformation } from "effect"
1830 *
1831 * const schema = Schema.String.pipe(
1832 * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromJsonString())
1833 * )
1834 * Schema.decodeSync(schema)("{\"ok\":true}") // => { ok: true }
1835 * ```
1836 *
1837 * @see {@link uint8ArrayFromBase64String}
1838 * @see {@link fromFormData}
1839 *
1840 * @category decoding
1841 * @since 4.0.0
1842 */
1843export function fromJsonString(options?: {
1844 readonly reviver?: Parameters<typeof JSON.parse>[1] | undefined
1845 readonly replacer?: SchemaGetter.JsonReplacer | undefined
1846 readonly space?: Parameters<typeof JSON.stringify>[2] | undefined
1847}): Transformation<unknown, string> {
1848 return new Transformation(
1849 SchemaGetter.parseJson(options ?? {}),
1850 SchemaGetter.stringifyJson(options)
1851 )
1852}
1853
1854/**
1855 * Decodes a `FormData` instance into a nested record using bracket-path keys and
1856 * encodes object-like values back into `FormData`.
1857 *
1858 * **When to use**
1859 *
1860 * Use when you need a schema transformation for form or multipart payloads
1861 * whose keys, such as `user[name]` or `items[0]`, should become nested data.
1862 *
1863 * **Details**
1864 *
1865 * Decode preserves string and `Blob` leaves. Encode flattens nested objects and
1866 * arrays into bracket-path entries and returns an empty `FormData` for
1867 * non-object inputs.
1868 *
1869 * **Example** (Decoding FormData)
1870 *
1871 * ```ts import.meta.vitest
1872 * import { Schema, SchemaTransformation } from "effect"
1873 *
1874 * const schema = Schema.instanceOf(FormData).pipe(
1875 * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromFormData)
1876 * )
1877 * const formData = new FormData()
1878 * formData.append("user[name]", "Alice")
1879 * Schema.decodeSync(schema)(formData) // => { user: { name: "Alice" } }
1880 * ```
1881 *
1882 * @see {@link fromURLSearchParams}
1883 * @see {@link fromJsonString}
1884 *
1885 * @category decoding
1886 * @since 4.0.0
1887 */
1888export const fromFormData = new Transformation<unknown, FormData>(
1889 SchemaGetter.decodeFormData(),
1890 SchemaGetter.encodeFormData()
1891)
1892
1893/**
1894 * Decodes `URLSearchParams` into a nested record using bracket-path keys and
1895 * encodes object-like values back into `URLSearchParams`.
1896 *
1897 * **When to use**
1898 *
1899 * Use when you need a schema transformation for query strings whose keys, such
1900 * as `filter[name]` or `items[0]`, should become nested data.
1901 *
1902 * **Details**
1903 *
1904 * Decode produces string leaves. Encode flattens nested objects and arrays into
1905 * bracket-path entries and returns empty `URLSearchParams` for non-object
1906 * inputs.
1907 *
1908 * **Example** (Decoding URLSearchParams)
1909 *
1910 * ```ts import.meta.vitest
1911 * import { Schema, SchemaTransformation } from "effect"
1912 *
1913 * const schema = Schema.instanceOf(URLSearchParams).pipe(
1914 * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromURLSearchParams)
1915 * )
1916 * Schema.decodeSync(schema)(new URLSearchParams("user[name]=Alice")) // => { user: { name: "Alice" } }
1917 * ```
1918 *
1919 * @see {@link fromFormData}
1920 * @see {@link fromJsonString}
1921 *
1922 * @category decoding
1923 * @since 4.0.0
1924 */
1925export const fromURLSearchParams = new Transformation<unknown, URLSearchParams>(
1926 SchemaGetter.decodeURLSearchParams(),
1927 SchemaGetter.encodeURLSearchParams()
1928)
1929
1930/**
1931 * Decodes a numeric time-zone offset in milliseconds into a
1932 * `DateTime.TimeZone.Offset` and encodes it back to the offset number.
1933 *
1934 * **When to use**
1935 *
1936 * Use when you need a schema transformation to represent fixed-offset time
1937 * zones with numeric millisecond offsets.
1938 *
1939 * **Details**
1940 *
1941 * Decode uses `DateTime.zoneMakeOffset`; encode returns the offset's `offset`
1942 * field.
1943 *
1944 * @see {@link timeZoneFromString} for IANA or offset string encodings
1945 * @see {@link timeZoneNamedFromString} for IANA named-zone strings
1946 *
1947 * @category transforming
1948 * @since 4.0.0
1949 */
1950export const timeZoneOffsetFromNumber: Transformation<DateTime.TimeZone.Offset, number> = transform<
1951 DateTime.TimeZone.Offset,
1952 number
1953>({
1954 decode: (n) => DateTime.zoneMakeOffset(n),
1955 encode: (tz) => tz.offset
1956})
1957
1958/**
1959 * Decodes an IANA time-zone identifier string into a
1960 * `DateTime.TimeZone.Named` and encodes a named time zone back to its `id`.
1961 *
1962 * **When to use**
1963 *
1964 * Use when you need a schema transformation to accept only IANA time-zone
1965 * identifier strings and produce `DateTime.TimeZone.Named` values.
1966 *
1967 * **Details**
1968 *
1969 * Decode fails with `InvalidValue` when the string is not a valid IANA time-zone
1970 * identifier.
1971 *
1972 * @see {@link timeZoneFromString} for time-zone strings that may be either IANA identifiers or offset strings
1973 *
1974 * @category transforming
1975 * @since 4.0.0
1976 */
1977export const timeZoneNamedFromString: Transformation<DateTime.TimeZone.Named, string> = transformEffect<
1978 DateTime.TimeZone.Named,
1979 string
1980>({
1981 decode: (s, options) => {
1982 return Option.match(DateTime.zoneMakeNamed(s), {
1983 onNone: () =>
1984 Effect.fail(
1985 new SchemaIssue.InvalidValue(
1986 { expected: "a valid IANA time zone" },
1987 s,
1988 options
1989 )
1990 ),
1991 onSome: Effect.succeed
1992 })
1993 },
1994 encode: (tz) => Effect.succeed(tz.id)
1995})
1996
1997/**
1998 * Decodes a string into a `DateTime.TimeZone` and encodes a time zone back to
1999 * its string representation.
2000 *
2001 * **When to use**
2002 *
2003 * Use when you need a schema transformation to accept either an IANA time-zone
2004 * identifier or an offset string and produce a general `DateTime.TimeZone`.
2005 *
2006 * **Details**
2007 *
2008 * Accepted decode inputs include valid IANA identifiers and offset strings such
2009 * as `"+03:00"`. Decode fails with `InvalidValue` when the string cannot be
2010 * parsed as a time zone.
2011 *
2012 * @see {@link timeZoneNamedFromString} for IANA named-zone strings only
2013 * @see {@link timeZoneOffsetFromNumber} for fixed-offset zones encoded as numbers
2014 *
2015 * @category transforming
2016 * @since 4.0.0
2017 */
2018export const timeZoneFromString: Transformation<DateTime.TimeZone, string> = transformEffect<
2019 DateTime.TimeZone,
2020 string
2021>({
2022 decode: (s, options) => {
2023 return Option.match(DateTime.zoneFromString(s), {
2024 onNone: () =>
2025 Effect.fail(
2026 new SchemaIssue.InvalidValue(
2027 { expected: "a valid time zone" },
2028 s,
2029 options
2030 )
2031 ),
2032 onSome: Effect.succeed
2033 })
2034 },
2035 encode: (tz) => Effect.succeed(DateTime.zoneToString(tz))
2036})
2037
2038/**
2039 * Decodes a date-time string into a `DateTime.Utc` and encodes it back to an ISO
2040 * string.
2041 *
2042 * **When to use**
2043 *
2044 * Use when you need a schema transformation to decode date-time strings to a
2045 * normalized `DateTime.Utc` and encode back as a UTC ISO string.
2046 *
2047 * **Details**
2048 *
2049 * Decode accepts strings supported by `DateTime.make`, converts the result to
2050 * UTC, and fails with `InvalidValue` when parsing fails. Encode uses
2051 * `DateTime.formatIso`.
2052 *
2053 * @see {@link dateFromString} for decoding into JavaScript `Date`
2054 * @see {@link dateTimeZonedFromString} for ISO strings that should preserve zoned date-time information
2055 *
2056 * @category transforming
2057 * @since 4.0.0
2058 */
2059export const dateTimeUtcFromString: Transformation<DateTime.Utc, string> = transformEffect<
2060 DateTime.Utc,
2061 string
2062>({
2063 decode: (s, options) => {
2064 return Option.match(DateTime.make(s), {
2065 onNone: () =>
2066 Effect.fail(
2067 new SchemaIssue.InvalidValue(
2068 { expected: "a valid UTC DateTime string" },
2069 s,
2070 options
2071 )
2072 ),
2073 onSome: (result) => Effect.succeed(DateTime.toUtc(result))
2074 })
2075 },
2076 encode: (utc) => Effect.succeed(DateTime.formatIso(utc))
2077})
2078
2079/**
2080 * Decodes a zoned date-time string into a `DateTime.Zoned` and encodes it back
2081 * to an ISO zoned string.
2082 *
2083 * **When to use**
2084 *
2085 * Use when you need a schema transformation for ISO zoned date-time strings
2086 * that decode to `DateTime.Zoned` and encode with `DateTime.formatIsoZoned`.
2087 *
2088 * **Details**
2089 *
2090 * Decode uses `DateTime.makeZonedFromString` and fails with `InvalidValue` when
2091 * the input is not a valid zoned date-time. Encode uses
2092 * `DateTime.formatIsoZoned`.
2093 *
2094 * @see {@link dateTimeUtcFromString} for date-time strings that should decode to `DateTime.Utc` and encode as UTC ISO strings
2095 *
2096 * @category transforming
2097 * @since 4.0.0
2098 */
2099export const dateTimeZonedFromString: Transformation<DateTime.Zoned, string> = transformEffect<
2100 DateTime.Zoned,
2101 string
2102>({
2103 decode: (s, options) => {
2104 return Option.match(DateTime.makeZonedFromString(s), {
2105 onNone: () =>
2106 Effect.fail(
2107 new SchemaIssue.InvalidValue(
2108 { expected: "a valid Zoned DateTime string" },
2109 s,
2110 options
2111 )
2112 ),
2113 onSome: Effect.succeed
2114 })
2115 },
2116 encode: (zoned) => Effect.succeed(DateTime.formatIsoZoned(zoned))
2117})