Effect Schema Modeling
Use this skill to model domain data once for both static types and runtime validation.
When To Use
- •New domain entities or command payloads
- •Parsing API responses or untrusted input
- •Replacing ad-hoc manual validation logic
- •Enforcing invariants at module boundaries
Prerequisite Checks
- •Identify all untrusted boundaries (
HTTP, queue, tool input, file input). - •Identify required invariants (formats, enums, optional fields, timestamps).
- •Confirm where decode failures should map to typed domain errors.
Modeling Rules
- •Define entities with
Schema.Struct(...)and composable Schema primitives. - •Derive types from schemas (
type X = typeof X.Type). - •Decode unknown input at boundaries with
Schema.decodeUnknownSync(...)or Effect decoders. - •Prefer Effect Standard Schema support first; use Zod only when integration requires it.
- •Use
Schema.DateTimeUtcfor timestamp fields when applicable.
Workflow
- •Accept
unknownonly at validation entry points. - •Define request/entity schemas with domain names, not transport names.
- •Decode immediately at the boundary.
- •Map decode errors to typed domain failures when needed.
- •Pass strongly typed values through internal logic.
- •Reuse schemas for both runtime decode and static type derivation.
Patterns To Follow
- •Keep schema declarations near domain modules.
- •Use small composable schemas for repeated fields.
- •Add transform schemas only when transformation is part of domain rules.
- •Keep decode functions as the only place
unknownenters business logic.
Anti-Patterns
- •Accepting untyped objects deep in service code.
- •Repeating manual field validation in multiple modules.
- •Using schema decode only in tests but not production boundaries.
- •Returning raw decode errors directly from user-facing APIs without mapping.
Example: Decode At Boundary + Typed Error
typescript
import { Effect, Schema } from "effect";
class InvalidWebhookPayloadError extends Schema.TaggedError<InvalidWebhookPayloadError>(
"InvalidWebhookPayloadError",
)({
message: Schema.String,
cause: Schema.optional(Schema.Defect),
}) {}
const CreateUser = Schema.Struct({
email: Schema.String,
name: Schema.optional(Schema.String),
createdAt: Schema.DateTimeUtc,
});
type CreateUser = typeof CreateUser.Type;
const decodeCreateUser = Schema.decodeUnknown(CreateUser);
const createUserInputFromUnknown = (input: unknown): Effect.Effect<CreateUser, InvalidWebhookPayloadError> =>
decodeCreateUser(input).pipe(
Effect.mapError((cause) =>
new InvalidWebhookPayloadError({
message: "Webhook payload did not match CreateUser schema",
cause,
}),
),
);
Verification Checklist
- •Every untrusted boundary decodes once before business logic.
- •Domain types are derived from schemas, not duplicated manually.
- •Decode failures are mapped to typed domain errors where required.
- •Timestamp fields use
Schema.DateTimeUtcwhere applicable. - •No lazy
unknownflows remain in internal logic.