Error Formatters
On this page
When you're working with Effect Schema and encounter errors during decoding, or encoding functions, you can format these errors in two different ways: using the TreeFormatter
or the ArrayFormatter
.
TreeFormatter (default)
The TreeFormatter
is the default method for formatting errors. It organizes errors in a tree structure, providing a clear hierarchy of issues.
Here's an example of how it works:
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person )constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (TreeFormatter .formatErrorSync (result .left ))}/*Decoding failed:{ readonly name: string; readonly age: number }└─ ["name"]└─ is missing*/
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person )constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (TreeFormatter .formatErrorSync (result .left ))}/*Decoding failed:{ readonly name: string; readonly age: number }└─ ["name"]└─ is missing*/
In this example, the tree error message is structured as follows:
{ readonly name: string; readonly age: number }
represents the schema, providing a visual representation of the expected structure. This can be customized by using annotations likeidentifier
,title
, ordescription
.["name"]
points to the problematic property, in this case, the"name"
property.is missing
details the specific error for the"name"
property.
Customizing the Output
The default error message represents the involved schemas in a TypeScript-like syntax:
ts
{ readonly name: string; readonly age: number }
ts
{ readonly name: string; readonly age: number }
You can customize this output by adding annotations such as identifier
, title
, or description
.
These annotations are applied in this order of priority and allow for a more concise and clear representation in error messages.
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number }).annotations ({title : "Person" }) // Adding a title annotationconstresult =Schema .decodeUnknownEither (Person )({})if (Either .isLeft (result )) {console .error (TreeFormatter .formatErrorSync (result .left ))}/*Person└─ ["name"]└─ is missing*/
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number }).annotations ({title : "Person" }) // Adding a title annotationconstresult =Schema .decodeUnknownEither (Person )({})if (Either .isLeft (result )) {console .error (TreeFormatter .formatErrorSync (result .left ))}/*Person└─ ["name"]└─ is missing*/
In this modified example, by adding a title
annotation, the schema representation in the error message changes to "Person", providing a simpler and more understandable output. This helps in identifying the schema involved more quickly and improves the readability of the error messages.
Handling Multiple Errors
By default, decoding functions like decodeUnknownEither
return only the first encountered error. If you require a comprehensive list of all errors, you can modify the behavior by passing the { errors: "all" }
option:
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person , {errors : "all" })constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (TreeFormatter .formatErrorSync (result .left ))}/*Decoding failed:{ readonly name: string; readonly age: number }├─ ["name"]│ └─ is missing└─ ["age"]└─ is missing*/
ts
import {Schema ,TreeFormatter } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person , {errors : "all" })constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (TreeFormatter .formatErrorSync (result .left ))}/*Decoding failed:{ readonly name: string; readonly age: number }├─ ["name"]│ └─ is missing└─ ["age"]└─ is missing*/
This adjustment ensures that the formatter displays all errors related to the input, providing a more detailed diagnostic of what went wrong.
ParseIssueTitle Annotation
When a decoding or encoding operation fails, it's useful to have additional details in the default error message returned by TreeFormatter
to understand exactly which value caused the operation to fail. To achieve this, you can set an annotation that depends on the value undergoing the operation and can return an excerpt of it, making it easier to identify the problematic value. A common scenario is when the entity being validated has an id
field. The ParseIssueTitle
annotation facilitates this kind of analysis during error handling.
The type of the annotation is:
ts
export type ParseIssueTitleAnnotation = (issue: ParseIssue) => string | undefined
ts
export type ParseIssueTitleAnnotation = (issue: ParseIssue) => string | undefined
If you set this annotation on a schema and the provided function returns a string
, then that string is used as the title by TreeFormatter
, unless a message
annotation (which has the highest priority) has also been set. If the function returns undefined
, then the default title used by TreeFormatter
is determined with the following priorities:
identifier
annotationtitle
annotationdescription
annotation- default TypeScript-like syntax
Example
ts
import type {ParseResult } from "@effect/schema"import {Schema } from "@effect/schema"constgetOrderItemId = ({actual }:ParseResult .ParseIssue ) => {if (Schema .is (Schema .Struct ({id :Schema .String }))(actual )) {return `OrderItem with id: ${actual .id }`}}constOrderItem =Schema .Struct ({id :Schema .String ,name :Schema .String ,price :Schema .Number }).annotations ({identifier : "OrderItem",parseIssueTitle :getOrderItemId })constgetOrderId = ({actual }:ParseResult .ParseIssue ) => {if (Schema .is (Schema .Struct ({id :Schema .Number }))(actual )) {return `Order with id: ${actual .id }`}}constOrder =Schema .Struct ({id :Schema .Number ,name :Schema .String ,items :Schema .Array (OrderItem )}).annotations ({identifier : "Order",parseIssueTitle :getOrderId })constdecode =Schema .decodeUnknownSync (Order , {errors : "all" })// No id available, so the `identifier` annotation is used as the titledecode ({})/*throwsParseError: Order├─ ["id"]│ └─ is missing├─ ["name"]│ └─ is missing└─ ["items"]└─ is missing*/// An id is available, so the `parseIssueTitle` annotation is used as the titledecode ({id : 1 })/*throwsParseError: Order with id: 1├─ ["name"]│ └─ is missing└─ ["items"]└─ is missing*/decode ({id : 1,items : [{id : "22b",price : "100" }] })/*throwsParseError: Order with id: 1├─ ["name"]│ └─ is missing└─ ["items"]└─ ReadonlyArray<OrderItem>└─ [0]└─ OrderItem with id: 22b├─ ["name"]│ └─ is missing└─ ["price"]└─ Expected a number, actual "100"*/
ts
import type {ParseResult } from "@effect/schema"import {Schema } from "@effect/schema"constgetOrderItemId = ({actual }:ParseResult .ParseIssue ) => {if (Schema .is (Schema .Struct ({id :Schema .String }))(actual )) {return `OrderItem with id: ${actual .id }`}}constOrderItem =Schema .Struct ({id :Schema .String ,name :Schema .String ,price :Schema .Number }).annotations ({identifier : "OrderItem",parseIssueTitle :getOrderItemId })constgetOrderId = ({actual }:ParseResult .ParseIssue ) => {if (Schema .is (Schema .Struct ({id :Schema .Number }))(actual )) {return `Order with id: ${actual .id }`}}constOrder =Schema .Struct ({id :Schema .Number ,name :Schema .String ,items :Schema .Array (OrderItem )}).annotations ({identifier : "Order",parseIssueTitle :getOrderId })constdecode =Schema .decodeUnknownSync (Order , {errors : "all" })// No id available, so the `identifier` annotation is used as the titledecode ({})/*throwsParseError: Order├─ ["id"]│ └─ is missing├─ ["name"]│ └─ is missing└─ ["items"]└─ is missing*/// An id is available, so the `parseIssueTitle` annotation is used as the titledecode ({id : 1 })/*throwsParseError: Order with id: 1├─ ["name"]│ └─ is missing└─ ["items"]└─ is missing*/decode ({id : 1,items : [{id : "22b",price : "100" }] })/*throwsParseError: Order with id: 1├─ ["name"]│ └─ is missing└─ ["items"]└─ ReadonlyArray<OrderItem>└─ [0]└─ OrderItem with id: 22b├─ ["name"]│ └─ is missing└─ ["price"]└─ Expected a number, actual "100"*/
In the examples above, we can see how the parseIssueTitle
annotation helps provide meaningful error messages when decoding fails.
ArrayFormatter
The ArrayFormatter
offers an alternative method for formatting errors within @effect/schema
, organizing them into a more structured and easily navigable array format. This formatter is especially useful when you need a clear overview of all issues detected during the decoding or encoding processes.
The ArrayFormatter
formats errors as an array of objects, where each object represents a distinct issue and includes properties such as _tag
, path
, and message
. This structured format can help developers quickly identify and address multiple issues in data processing.
Here's an example of how it works:
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person )constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (ArrayFormatter .formatErrorSync (result .left ))}/*Decoding failed:[ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' } ]*/
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person )constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (ArrayFormatter .formatErrorSync (result .left ))}/*Decoding failed:[ { _tag: 'Missing', path: [ 'name' ], message: 'is missing' } ]*/
Each error is formatted as an object in an array, making it clear what the error is (is missing
), where it occurred (name
), and its type (Missing
).
Handling Multiple Errors
By default, decoding functions like decodeUnknownEither
return only the first encountered error. If you require a comprehensive list of all errors, you can modify the behavior by passing the { errors: "all" }
option:
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person , {errors : "all" })constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (ArrayFormatter .formatErrorSync (result .left ))}/*Decoding failed:[{ _tag: 'Missing', path: [ 'name' ], message: 'is missing' },{ _tag: 'Missing', path: [ 'age' ], message: 'is missing' }]*/
ts
import {ArrayFormatter ,Schema } from "@effect/schema"import {Either } from "effect"constPerson =Schema .Struct ({name :Schema .String ,age :Schema .Number })constdecode =Schema .decodeUnknownEither (Person , {errors : "all" })constresult =decode ({})if (Either .isLeft (result )) {console .error ("Decoding failed:")console .error (ArrayFormatter .formatErrorSync (result .left ))}/*Decoding failed:[{ _tag: 'Missing', path: [ 'name' ], message: 'is missing' },{ _tag: 'Missing', path: [ 'age' ], message: 'is missing' }]*/
React Hook Form
If you are working with React and need form validation, @hookform/resolvers
offers an adapter for @effect/schema
, which can be integrated with React Hook Form for enhanced form validation processes. This integration allows you to leverage the powerful features of @effect/schema
within your React applications.
For more detailed instructions and examples on how to integrate @effect/schema
with React Hook Form using @hookform/resolvers
, you can visit the official npm package page:
React Hook Form Resolvers