Schema and validation
Decode, encode and derive typed values without coupling the core to a provider.
better-effect-schema is the provider-neutral schema package for applications
that need runtime decoding alongside better-result and better-effect. The
recommended application path is a real provider schema imported through one of
the three preconfigured provider subpaths. The root entry point uses Standard
Schema and does not import Zod, Valibot or ArkType.
Install and choose a provider
bun add better-effect-schema better-effect better-result
# Choose one provider and install its peer:
bun add zod # or: bun add valibot / arktype| Import | Purpose | Dependency |
|---|---|---|
better-effect-schema | Standard Schema classes, operations and failures | Standard Schema, better-result, better-effect types |
better-effect-schema/zod | Preconfigured Zod 4 Schema; choose for its ecosystem, codecs and class derivations | Zod |
better-effect-schema/valibot | Preconfigured Valibot Schema; choose for modular APIs and small bundles | Valibot |
better-effect-schema/arktype | Preconfigured ArkType Schema; choose for concise definitions and inferred runtime types | ArkType |
The provider packages are optional peers. Importing the root does not select a provider or add framework lifecycle.
Decode and encode
Schema.Class describes a decoded class backed by the provider schema. Decode
and encode operations return better-result values; expected validation
failures are not thrown. This is the shape to use for HTTP, MQ and other
untrusted boundaries where runtime validation and a stable application type
matter.
import * as z from 'zod'
import { Result } from 'better-result'
import { Schema as CoreSchema } from 'better-effect-schema'
import { Schema } from 'better-effect-schema/zod'
const UserSchema = z.object({
id: z.string().min(1),
email: z.email(),
displayName: z.string().min(1)
})
class User extends Schema.Class<User>('example/User')(UserSchema) {}
const decoded = Schema.decodeUnknown(User, {
id: 'user-1',
email: 'ada@example.com',
displayName: 'Ada Lovelace'
})
if (Result.isError(decoded)) throw decoded.error
const encoded = CoreSchema.encode(User, decoded.value)
if (Result.isError(encoded)) throw encoded.errorUse Schema.decode when the encoded input type is known, Schema.decodeUnknown
for data from JSON, HTTP, queues or other untrusted sources, and Schema.make
for decoded constructor props. Use the corresponding Async operation for an
asynchronous validator or encoder. Transformed values must provide an explicit
encoder; the package never invents a JSON representation.
Provider adapters
Each provider subpath exports a ready-to-use Schema facade. Import the one
that matches your native schema library; aliases let multiple providers
coexist in one module:
import * as z from 'zod'
import { Schema as ZodSchema } from 'better-effect-schema/zod'
import { Schema as ValibotSchema } from 'better-effect-schema/valibot'
import { Schema as ArkTypeSchema } from 'better-effect-schema/arktype'
class Person extends ZodSchema.Class<Person>('example/Person')({
id: z.int().positive(),
name: z.string()
}) {}HTTP, MQ codecs and other integrations accept Standard Schema values directly,
so a native schema can be passed to Codec.standardSchema without teaching the
core package about a provider. Use the root package for provider-neutral
Standard Schema code; use a provider subpath when you want that provider's
native class factories, normalized Result failures or derivations. The
advanced Schema.with API is for authoring custom adapters or intentionally
configured capability sets, not for built-in providers.
Raw Standard Schema: an adapter escape hatch
Most applications should not hand-write a StandardSchemaV1 object. Prefer a
provider-backed schema and adapter so validation, issues and inferred types stay
consistent. Implement raw Standard Schema only when you are authoring a schema
provider/adapter or integrating a library that already exposes Standard Schema
and cannot be represented by a supported provider. It is an interoperability
escape hatch, not the default application example.
Tagged classes and errors
Schema.TaggedClass and Schema.TaggedError preserve literal tags and use the
better-result failure protocol. They can be yielded from Result.gen and
composed in an Effect.gen Program without introducing another error model.
The package is intentionally pure: it has no Runtime, Scope, framework or database lifecycle. See the package catalog for how it fits with HTTP and the other integrations.