Skip to content

Transformations

A transformation connects two schemas with a pair of functions — one that runs while decoding (EncodedType) and one that runs while encoding (TypeEncoded). This is how a schema can store a date as a string on the wire yet hand you a real Date, or accept "42" and give you the number 42.

import { Schema, SchemaGetter } from "effect"
// `from.pipe(Schema.decodeTo(to, { decode, encode }))`
// - decode runs Encoded -> Type (here: string -> number)
// - encode runs Type -> Encoded (here: number -> string)
const NumberFromString = Schema.String.pipe(
Schema.decodeTo(Schema.Number, {
decode: SchemaGetter.Number(), // parse the string into a number
encode: SchemaGetter.String() // serialize the number back to a string
})
)
console.log(Schema.decodeUnknownSync(NumberFromString)("123")) // 123 (number)
console.log(Schema.encodeUnknownSync(NumberFromString)(123)) // "123" (string)

Schema.decodeTo(to, transformation) is curried: you call it with the target schema and then pipe the source schema into it. The result has Encoded = from.Encoded and Type = to.Type.

The transformation object holds two getters:

  • decode: turns the source’s decoded value into the target’s encoded value (used when decoding).
  • encode: turns the target’s encoded value back into the source’s decoded value (used when encoding).

SchemaGetter provides ready-made getters. SchemaGetter.transform(f) lifts a plain function; SchemaGetter.Number(), SchemaGetter.String(), SchemaGetter.trim(), and friends cover common conversions.

import { Schema, SchemaGetter } from "effect"
// Trim surrounding whitespace while decoding (and again while encoding).
const TrimmedString = Schema.String.pipe(
Schema.decode({
decode: SchemaGetter.trim(),
encode: SchemaGetter.passthrough()
})
)
console.log(Schema.decodeUnknownSync(TrimmedString)(" hello ")) // "hello"

Decoding often involves parsing that may not succeed. Use SchemaGetter.transformOrFail, which returns an Effect and reports failures as a SchemaIssue. The failure is woven into the normal decode error tree, so it benefits from path tracking and error formatting.

import { Effect, Option, Schema, SchemaGetter, SchemaIssue } from "effect"
// A schema that decodes a string into an integer, failing cleanly on bad input.
const IntFromString = Schema.String.pipe(
Schema.decodeTo(Schema.Number, {
decode: SchemaGetter.transformOrFail((s: string) => {
const n = Number.parseInt(s, 10)
return Number.isNaN(n)
? Effect.fail(
new SchemaIssue.InvalidValue(Option.some(s), {
message: "expected an integer string"
})
)
: Effect.succeed(n)
}),
encode: SchemaGetter.String()
})
)
console.log(Schema.decodeUnknownSync(IntFromString)("7")) // 7
// Schema.decodeUnknownSync(IntFromString)("abc") -> throws "expected an integer string"

Schema.encodeTo is the mirror image: it lets you name the encoded schema first. to.pipe(Schema.encodeTo(encodedSchema, { decode, encode })) reads naturally when you think of a value being encoded outward. It is otherwise equivalent to decodeTo with the schemas swapped.

import { Schema, SchemaGetter } from "effect"
const NumberFromString = Schema.Number.pipe(
Schema.encodeTo(Schema.String, {
decode: SchemaGetter.transform((s: string) => Number(s)),
encode: SchemaGetter.transform((n: number) => String(n))
})
)

Because a transformation produces a regular schema, you can keep piping. A common pattern is to parse a JSON string and then validate its shape — chaining a string→JSON transformation into a struct schema.

import { Schema } from "effect"
const Payload = Schema.Struct({
id: Schema.String,
count: Schema.Number
})
// Encoded: a JSON string -> Type: the validated Payload object.
const PayloadFromJson = Schema.fromJsonString(Payload)
const value = Schema.decodeUnknownSync(PayloadFromJson)(
'{"id":"a","count":3}'
)
console.log(value) // { id: "a", count: 3 }

Many useful transformations ship ready to use, so you rarely write the getters by hand:

| Schema | Encoded → Type | | -------------------------- | --------------------- | | Schema.NumberFromString | stringnumber | | Schema.BigIntFromString | stringbigint | | Schema.DateFromString | stringDate | | Schema.Trim | string → trimmed string | | Schema.fromJsonString(S) | JSON stringS |

Reach for these first; drop down to decodeTo/encodeTo only when you need a conversion that is not already provided.