Effect by Example: Parsing JSON with Schema

Tags:

Effect has a powerful type validation library (like Zod) called Schema.

Basic Example

Here is a basic example:

import {
import Schema
Schema
} from "effect";
const
const testSchema: Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>
testSchema
=
import Schema
Schema
.
function Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>(fields: {
name: typeof Schema.String;
age: typeof Schema.Number;
}): Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}> (+1 overload)

@since3.10.0

Struct
({
name: typeof Schema.String
name
:
import Schema
Schema
.
class String
export String

@since3.10.0

String
,
age: typeof Schema.Number
age
:
import Schema
Schema
.
class Number
export Number

@since3.10.0

Number
,
});
const
const data: {
name: string;
age: number;
}
data
= {
name: string
name
: "Bob",
age: number
age
: 30,
};
const
const string: string
string
=
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
const data: {
name: string;
age: number;
}
data
);
const
const decoded: {
readonly name: string;
readonly age: number;
}
decoded
=
import Schema
Schema
.
decodeUnknownSync<{
readonly name: string;
readonly age: number;
}, {
readonly name: string;
readonly age: number;
}>(schema: Schema.Schema<{
readonly name: string;
readonly age: number;
}, {
readonly name: string;
readonly age: number;
}, never>, options?: ParseOptions): (u: unknown, overrideOptions?: ParseOptions) => {
readonly name: string;
readonly age: number;
}
export decodeUnknownSync

@throwsParseError

@since3.10.0

decodeUnknownSync
(
const testSchema: Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>
testSchema
)(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any

Converts a JavaScript Object Notation (JSON) string into an object.

@paramtext A valid JSON string.

@paramreviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is.

parse
(
const string: string
string
));

Here, the last line will throw if the validation fails.

Transformations

Schema’s superpower is its ability to describe two-way transformations.

The type of a Schema has two type parameters: the input type and the output type.

type
type Schema<Decoded, Encoded> = {}
Schema
<
function (type parameter) Decoded in type Schema<Decoded, Encoded>
Decoded
,
function (type parameter) Encoded in type Schema<Decoded, Encoded>
Encoded
> = {};

We can then use such a schema to transform between the two types.

Consider the Schema.parseJson function, which takes a schema and returns a new schema which transforms between JSON and the schema’s type.

Combined with other schemas, which can serialize/deserialize types that are not JSON-compatible, we can build a complex schema capable of encoding/decoding any type.

import {
import Schema
Schema
} from "effect";
const
const testSchema: Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>
testSchema
=
import Schema
Schema
.
function Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>(fields: {
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}): Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}> (+1 overload)

@since3.10.0

Struct
({
name: Schema.Set$<typeof Schema.String>
name
:
import Schema
Schema
.
Set<typeof Schema.String>(value: typeof Schema.String): Schema.Set$<typeof Schema.String>
export Set

@ignore

Set
(
import Schema
Schema
.
class String
export String

@since3.10.0

String
),
age: typeof Schema.Date
age
:
import Schema
Schema
.
class Date
export Date

This schema converts a string into a Date object using the new Date constructor. It ensures that only valid date strings are accepted, rejecting any strings that would result in an invalid date, such as new Date("Invalid Date").

@since3.10.0

Date
,
});
const
const testSchemaJson: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>>
testSchemaJson
=
import Schema
Schema
.
const parseJson: <Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>>(schema: Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>, options?: Schema.ParseJsonOptions) => Schema.transform<...> (+1 overload)

The ParseJson combinator provides a method to convert JSON strings into the unknown type using the underlying functionality of JSON.parse. It also utilizes JSON.stringify for encoding.

You can optionally provide a ParseJsonOptions to configure both JSON.parse and JSON.stringify executions.

Optionally, you can pass a schema Schema<A, I, R> to obtain an A type instead of unknown.

@example

import * as assert from "node:assert"
import * as Schema from "effect/Schema"
assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson())(`{"a":"1"}`), { a: "1" })
assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson(Schema.Struct({ a: Schema.NumberFromString })))(`{"a":"1"}`), { a: 1 })

@since3.10.0

parseJson
(
const testSchema: Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>
testSchema
);
const
const data: {
name: Set<string>;
age: Date;
}
data
= {
name: Set<string>
name
: new
var Set: SetConstructor
new <string>(iterable?: Iterable<string> | null | undefined) => Set<string> (+1 overload)
Set
(["Bob", "Alice"]),
age: Date
age
: new
var Date: DateConstructor
new () => Date (+3 overloads)
Date
(),
};
const
const string: string
string
=
import Schema
Schema
.
encodeSync<{
readonly name: Set<string>;
readonly age: Date;
}, string>(schema: Schema.Schema<{
readonly name: Set<string>;
readonly age: Date;
}, string, never>, options?: ParseOptions): (a: {
readonly name: Set<string>;
readonly age: Date;
}, overrideOptions?: ParseOptions) => string
export encodeSync

@since3.10.0

encodeSync
(
const testSchemaJson: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>>
testSchemaJson
)(
const data: {
name: Set<string>;
age: Date;
}
data
);
const
const decoded: {
readonly name: Set<string>;
readonly age: Date;
}
decoded
=
import Schema
Schema
.
decodeSync<{
readonly name: Set<string>;
readonly age: Date;
}, string>(schema: Schema.Schema<{
readonly name: Set<string>;
readonly age: Date;
}, string, never>, options?: ParseOptions): (i: string, overrideOptions?: ParseOptions) => {
readonly name: Set<string>;
readonly age: Date;
}
export decodeSync

@since3.10.0

decodeSync
(
const testSchemaJson: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: Schema.Set$<typeof Schema.String>;
age: typeof Schema.Date;
}>>
testSchemaJson
)(
const string: string
string
);

Consuming Schema from Effects

All of the examples so far on this page have been using the *Sync variants of the Schema functions. These all return synchronously, but throw if the validation fails, or a transformation is asynchronous.

To consume these apis as Effects, just drop the Sync suffix.

import {
import Schema
Schema
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect";
const
const schema: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>>
schema
=
import Schema
Schema
.
const parseJson: <Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>>(schema: Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>, options?: Schema.ParseJsonOptions) => Schema.transform<...> (+1 overload)

The ParseJson combinator provides a method to convert JSON strings into the unknown type using the underlying functionality of JSON.parse. It also utilizes JSON.stringify for encoding.

You can optionally provide a ParseJsonOptions to configure both JSON.parse and JSON.stringify executions.

Optionally, you can pass a schema Schema<A, I, R> to obtain an A type instead of unknown.

@example

import * as assert from "node:assert"
import * as Schema from "effect/Schema"
assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson())(`{"a":"1"}`), { a: "1" })
assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.parseJson(Schema.Struct({ a: Schema.NumberFromString })))(`{"a":"1"}`), { a: 1 })

@since3.10.0

parseJson
(
import Schema
Schema
.
function Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>(fields: {
name: typeof Schema.String;
age: typeof Schema.Number;
}): Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}> (+1 overload)

@since3.10.0

Struct
({
name: typeof Schema.String
name
:
import Schema
Schema
.
class String
export String

@since3.10.0

String
,
age: typeof Schema.Number
age
:
import Schema
Schema
.
class Number
export Number

@since3.10.0

Number
,
}),
);
const
const main: Effect.Effect<void, ParseError, never>
main
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<string, ParseError, never>> | YieldWrap<Effect.Effect<{
readonly name: string;
readonly age: number;
}, ParseError, never>>, void>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
const
const data: {
name: string;
age: number;
}
data
= {
name: string
name
: "Bob",
age: number
age
: 30,
};
const
const string: string
string
= yield*
import Schema
Schema
.
const encode: <{
readonly name: string;
readonly age: number;
}, string, never>(schema: Schema.Schema<{
readonly name: string;
readonly age: number;
}, string, never>, options?: ParseOptions) => (a: {
readonly name: string;
readonly age: number;
}, overrideOptions?: ParseOptions) => Effect.Effect<...>

@since3.10.0

encode
(
const schema: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>>
schema
)(
const data: {
name: string;
age: number;
}
data
);
const
const decoded: {
readonly name: string;
readonly age: number;
}
decoded
= yield*
import Schema
Schema
.
const decode: <{
readonly name: string;
readonly age: number;
}, string, never>(schema: Schema.Schema<{
readonly name: string;
readonly age: number;
}, string, never>, options?: ParseOptions) => (i: string, overrideOptions?: ParseOptions) => Effect.Effect<...>

@since3.10.0

decode
(
const schema: Schema.transform<Schema.SchemaClass<unknown, string, never>, Schema.Struct<{
name: typeof Schema.String;
age: typeof Schema.Number;
}>>
schema
)(
const string: string
string
);
});