Introduction to Effect Schema
Welcome to the documentation for effect/Schema, a module for defining and using schemas to validate and transform data in TypeScript.
The effect/Schema module allows you to define schema values that describe the structure and data types of your data. Once defined, you can leverage these schemas to perform a range of operations, including:
| Operation | Description |
|---|---|
| Decoding | Transforming data from an input type Encoded to an output type Type. |
| Encoding | Converting data from an output type Type back to an input type Encoded. |
| Asserting | Verifying that a value adheres to the schema’s output type Type. |
| Standard Schema | Generate a Standard Schema V1. |
| Arbitraries | Generate Arbitraries for fast-check testing. |
| JSON Schemas | Create JSON Schemas for the encoded representation of schemas. |
| Equivalence | Create an Equivalence based on a schema. |
| Formatting | Create a Formatter based on a schema. |
Requirements
- TypeScript 5.9 or newer. TypeScript 7 is recommended for the best performance and compatibility with Effect’s TypeScript tooling.
- The
strictflag enabled in yourtsconfig.jsonfile. - (Optional) The
exactOptionalPropertyTypesflag enabled in yourtsconfig.jsonfile.
{ "compilerOptions": { "strict": true, "exactOptionalPropertyTypes": true, // optional },}The exactOptionalPropertyTypes Option
The effect/Schema module takes advantage of the exactOptionalPropertyTypes option of tsconfig.json. This option affects how optional properties are typed (to learn more about this option, you can refer to the official TypeScript documentation).
Example (With exactOptionalPropertyTypes Enabled)
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.optionalKey(Schema.String),})
type T = typeof Person.Type/*type T = { readonly name?: string;}*/
// @errors: 2379Schema.decodeSync(Person)({ name: undefined })With exactOptionalPropertyTypes enabled, name may be omitted, but when the property is present its value must be a string. TypeScript does not widen the property type to string | undefined, so the type checker rejects an explicit { name: undefined }.
Example (With exactOptionalPropertyTypes Disabled)
If, for some reason, you can’t enable the exactOptionalPropertyTypes option (perhaps due to conflicts with other third-party libraries), you can still use effect/Schema. However, there will be a mismatch between the types and the runtime behavior:
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.optionalKey(Schema.String),})
type T = typeof Person.Type/*type T = { readonly name?: string | undefined;}*/
// No type error, but a decoding failure occursSchema.decodeSync(Person)({ name: undefined })/*throwsSchemaError: Expected string at ["name"]*/In this case, the type of name is widened to string | undefined, which means the type checker won’t catch the invalid value (undefined). However, during decoding, you’ll encounter an error, indicating that undefined is not allowed.
Schema Views
A schema is an immutable value that describes the structure of your data. The same schema value can be viewed through different interfaces, depending on which type-level information an API needs:
| View | Type-level information retained |
|---|---|
Top |
No specific type information; accepts any schema |
Schema<Type> |
Decoded type |
Decoder<Type, DecodingServices> |
Decoded type and services required for decoding |
Encoder<Encoded, EncodingServices> |
Encoded type and services required for encoding |
Codec<Type, Encoded, DecodingServices, EncodingServices> |
Decoded type, encoded type, and services required in both directions |
For example, the Codec view retains all four directional type parameters:
┌─── Type of the decoded value │ ┌─── Encoded type (input/output) │ │ ┌─── Services required for decoding │ │ │ ┌─── Services required for encoding ▼ ▼ ▼ ▼Codec<Type, Encoded, DecodingServices, EncodingServices>These type parameters have the following meanings:
| Parameter | Description |
|---|---|
| Type | The type of value produced by decoding. |
| Encoded | The encoded representation accepted during decoding and produced during encoding. It defaults to Type. |
| DecodingServices | The services required during decoding. It defaults to never, meaning that decoding has no service requirements. |
| EncodingServices | The services required during encoding. It defaults to never, meaning that encoding has no service requirements. |
Examples
Schema<string>is a type-only view of any schema whose decoded type isstring.Decoder<number>retains the decoded type but does not constrain the encoded type or encoding services.Encoder<string>retains the encoded type but does not constrain the decoded type or decoding services.Codec<string>is shorthand forCodec<string, string, never, never>.Codec<number, string>represents a codec that decodes anumberfrom astring, encodes anumberto astring, and requires no services.
Understanding Schema Values
Schema Values. Schema values are immutable descriptions of data. Combinators that compose, refine, or transform a schema return a new schema without modifying the original.
Schema Interpreters. A schema can be interpreted by different interpreters to produce operations such as decoding, encoding, formatting, and arbitrary generation.
Understanding Decoding and Encoding
When working with data in TypeScript, you often need to handle data coming from or being sent to external systems. This data may not always match the format or types you expect, especially when dealing with user input, data from APIs, or data stored in different formats. To handle these discrepancies, we use decoding and encoding.
| Term | Description |
|---|---|
| Decoding | Converts a value from its encoded type E to its type T. |
| Encoding | Converts a value from its type T to its encoded type E. |
For example, consider an HTTP endpoint whose request and response bodies contain a finite number represented as a JSON string. After the request body is parsed as JSON, decoding converts "42" into the number 42. Before sending the response, encoding converts 42 back into "42", which can then be serialized as JSON.
Below is a diagram that shows the relationship between encoding and decoding through the Codec<T, E, RD, RE> view:
┌─────────┐ ┌───┐ ┌───┐ ┌─────────┐│ unknown │ │ T │ │ E │ │ unknown │└─────────┘ └───┘ └───┘ └─────────┘ │ │ │ │ │ is │ │ │ │───────────────────▶ │ │ │ │ │ │ │ asserts │ │ │ │───────────────────▶ │ │ │ │ │ │ │ encodeUnknownEffect │ │ │────────────────────────────────────────────▶ │ │ │ │ │ │ │ encodeEffect │ │ │ │────────────────────────▶ │ │ │ │ │ │ │ decodeEffect │ │ │ ◀────────────────────────│ │ │ │ │ │ │ │ decodeUnknownEffect │ │ │ ◀──────────────────────────────────────────────────│ │ │ │ │The diagram shows the Effect-based interpreters because they preserve the RD and RE service requirements. The Sync, Result, Exit, Option, and Promise variants follow the same directions but can only be used when the corresponding operation requires no services.
We’ll illustrate these concepts using Schema.FiniteFromString, which can be viewed as a Codec<number, string>. It decodes a string into a finite number, encodes a finite number as a string, and requires no services in either direction.
Encoding
When we talk about “encoding,” we are referring to the process of changing a finite number into a string. To put it simply, it’s the act of converting data from one format to another.
Decoding
Conversely, “decoding” entails transforming a string into a finite number. It’s essentially the reverse operation of encoding, where data is returned to its original form.
Decoding From Unknown
Decoding from unknown involves two key steps:
-
Checking: Initially, we verify that the input data (which is of the
unknowntype) matches the expected structure. In our specific case, this means ensuring that the input is indeed astring. -
Decoding: Following the successful check, we proceed to convert the
stringinto a finitenumber. This process completes the decoding operation, where the data is both validated and transformed.
Encoding From Unknown
Encoding from unknown involves two key steps:
-
Checking: Initially, we verify that the input data (which is of the
unknowntype) matches the expected structure. In our specific case, this means ensuring that the input is indeed a finitenumber. -
Encoding: Following the successful check, we proceed to convert the finite
numberinto astring. This process completes the encoding operation, where the data is both validated and transformed.
Round-Trip
A highly desirable property of a schema is that an encoding-decoding round trip returns a value equivalent to the original:
decode(encode(value)) ≈ valueThe round trip starts with encoding because value has type T: encoding produces an E, which is then decoded back into a T.
This property is not guaranteed. Some transformations intentionally normalize or discard information during encoding or decoding.