Claude
Skills
Sign in
Back

schema

Included with Lifetime
$97 forever

This skill should be used when the user asks about "Effect Schema", "Schema.Struct", "Schema.decodeUnknown", "data validation", "parsing", "Schema.transform", "Schema filters", "Schema annotations", "JSON Schema", "Schema.Class", "Schema branded types", "encoding", "decoding", "Schema.parseJson", or needs to understand how Effect handles data validation and transformation.

General

What this skill does


# Schema in Effect

## Overview

Effect Schema provides:

- **Type-safe validation** - Runtime checks with TypeScript inference
- **Bidirectional transformation** - Decode from external, encode for output
- **Composable schemas** - Build complex types from primitives
- **Error messages** - Detailed, customizable validation errors
- **Interop** - JSON Schema, Pretty Printing, Arbitrary generation

## Schema Best Practices

### 1. Tagged Unions Over Optional Properties

**AVOID optional properties. USE tagged unions instead.** This makes states explicit and enables exhaustive pattern matching.

```typescript
// ❌ BAD: Optional properties hide state complexity
const User = Schema.Struct({
  id: Schema.String,
  name: Schema.String,
  email: Schema.optional(Schema.String),
  verifiedAt: Schema.optional(Schema.Date),
  suspendedReason: Schema.optional(Schema.String),
});
// Unclear: Can a user be both verified AND suspended? What if email is missing?

// ✅ GOOD: Tagged union makes states explicit
const User = Schema.Union(
  Schema.Struct({
    _tag: Schema.Literal("Unverified"),
    id: Schema.String,
    name: Schema.String,
  }),
  Schema.Struct({
    _tag: Schema.Literal("Active"),
    id: Schema.String,
    name: Schema.String,
    email: Schema.String,
    verifiedAt: Schema.Date,
  }),
  Schema.Struct({
    _tag: Schema.Literal("Suspended"),
    id: Schema.String,
    name: Schema.String,
    email: Schema.String,
    suspendedReason: Schema.String,
  }),
);
// Clear: Each state has exactly the fields it needs
```

**Why tagged unions:**

- No impossible states (suspended user always has a reason)
- Exhaustive matching catches missing cases
- Self-documenting state machine
- Works perfectly with Match.tag

### 2. Class-Based Schemas Over Struct Schemas

**PREFER Schema.Class over Schema.Struct.** Classes give you methods, Schema.is() type guards, and better ergonomics.

```typescript
// ❌ AVOID: Plain Struct (no methods, no Schema.is() support)
const UserStruct = Schema.Struct({
  id: Schema.String,
  firstName: Schema.String,
  lastName: Schema.String,
  email: Schema.String,
});
type User = Schema.Schema.Type<typeof UserStruct>;

// ✅ PREFER: Class-based Schema
class User extends Schema.Class<User>("User")({
  id: Schema.String,
  firstName: Schema.String,
  lastName: Schema.String,
  email: Schema.String,
}) {
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  get emailDomain() {
    return this.email.split("@")[1];
  }

  withEmail(email: string) {
    return new User({ ...this, email });
  }
}

// Usage:
const user = Schema.decodeUnknownSync(User)(data);
console.log(user.fullName); // "John Doe"
console.log(Schema.is(User)(user)); // true - use Schema.is() for type checks
```

**For tagged unions with classes:**

```typescript
class Unverified extends Schema.TaggedClass<Unverified>()("Unverified", {
  id: Schema.String,
  name: Schema.String,
}) {}

class Active extends Schema.TaggedClass<Active>()("Active", {
  id: Schema.String,
  name: Schema.String,
  email: Schema.String,
  verifiedAt: Schema.Date,
}) {
  get isRecent() {
    return Date.now() - this.verifiedAt.getTime() < 86400000;
  }
}

class Suspended extends Schema.TaggedClass<Suspended>()("Suspended", {
  id: Schema.String,
  name: Schema.String,
  suspendedReason: Schema.String,
}) {}

const User = Schema.Union(Unverified, Active, Suspended);
type User = Schema.Schema.Type<typeof User>;
```

### 3. Schema.is() with Match Patterns

**USE Schema.is() as type guards in Match.when patterns.** This combines Schema validation with Match's exhaustive checking.

```typescript
import { Schema, Match } from "effect";

// Define schemas
class Circle extends Schema.TaggedClass<Circle>()("Circle", {
  radius: Schema.Number,
}) {
  get area() {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Schema.TaggedClass<Rectangle>()("Rectangle", {
  width: Schema.Number,
  height: Schema.Number,
}) {
  get area() {
    return this.width * this.height;
  }
}

const Shape = Schema.Union(Circle, Rectangle);
type Shape = Schema.Schema.Type<typeof Shape>;

// Use Schema.is() in Match patterns
const describeShape = (shape: Shape) =>
  Match.value(shape).pipe(
    Match.when(Schema.is(Circle), (c) => `Circle with radius ${c.radius}`),
    Match.when(Schema.is(Rectangle), (r) => `${r.width}x${r.height} rectangle`),
    Match.exhaustive,
  );

// Schema.is() also works for runtime type checking
const processUnknown = (input: unknown) => {
  if (Schema.is(Circle)(input)) {
    console.log(`Circle area: ${input.area}`);
  }
};
```

**Schema.is() vs Match.tag:**

```typescript
// Match.tag - when you already know it's the union type
const handleUser = (user: User) =>
  Match.value(user).pipe(
    Match.tag("Active", (u) => sendEmail(u.email)),
    Match.tag("Suspended", (u) => logSuspension(u.suspendedReason)),
    Match.tag("Unverified", () => sendVerificationReminder()),
    Match.exhaustive,
  );

// Schema.is() - when validating unknown data or need class features
const handleUnknown = (input: unknown) =>
  Match.value(input).pipe(
    Match.when(Schema.is(Active), (u) => u.isRecent), // Can use class methods
    Match.when(Schema.is(Suspended), () => false),
    Match.orElse(() => false),
  );
```

**NEVER access `._tag` directly:**

```typescript
// ❌ FORBIDDEN - direct ._tag access
if (user._tag === "Active") { ... }
const isActive = user._tag === "Active"

// ❌ FORBIDDEN - ._tag in type definitions
type UserTag = User["_tag"]  // Never extract _tag as a type

// ❌ FORBIDDEN - ._tag in array predicates
const hasActive = users.some((u) => u._tag === "Active")
const activeUsers = users.filter((u) => u._tag === "Active")
const activeCount = users.filter((u) => u._tag === "Active").length

// ✅ REQUIRED - Schema.is() as array predicate
const hasActive = users.some(Schema.is(Active))
const activeUsers = users.filter(Schema.is(Active))
const activeCount = users.filter(Schema.is(Active)).length

// ✅ REQUIRED - use Match.tag or Schema.is()
const handleUser = Match.type<User>().pipe(
  Match.tag("Active", (u) => ...),
  Match.exhaustive
)

// ✅ REQUIRED - Schema.is() for type guards
const isActive = Schema.is(Active)
if (isActive(user)) { ... }
```

### 4. Never Use Schema.Any or Schema.Unknown for Type Weakening

**`Schema.Any` and `Schema.Unknown` are ONLY permitted when the value is genuinely unconstrained at the domain level.** Using them to avoid writing a proper schema is type weakening and defeats the purpose of Schema-first modeling.

**Semantically correct uses (ALLOWED):**

```typescript
// ✅ Error `cause` capturing arbitrary caught exceptions - the value is genuinely unknown
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
  url: Schema.String,
  cause: Schema.Unknown,
}) {}

// ✅ A generic container that truly accepts any value (e.g., a cache, event metadata)
class CacheEntry extends Schema.Class<CacheEntry>("CacheEntry")({
  key: Schema.String,
  value: Schema.Unknown, // Cache genuinely stores arbitrary data
  ttl: Schema.Number,
}) {}

// ✅ Schema.parseJson without a target schema to get raw parsed JSON
const RawJson = Schema.parseJson(); // Produces Schema<unknown>
// This is fine as an intermediate step before further validation
```

**Type weakening (FORBIDDEN):**

```typescript
// ❌ FORBIDDEN: Lazy schema definition - write the actual shape
const UserResponse = Schema.Struct({
  data: Schema.Unknown, // What is "data"? Define it!
});

// ❌ FORBIDDEN: Avoiding nested schema definition
const ApiResponse = Schema.Struct({
  body: Schema.Any, // Write the actual body schema
});

// ❌ FORBIDDEN: Using Schema.Unknown for "I'll validate later"
const input: Schema.Schema<unknown> = Schema.Unknown;
// Define the correct schema upfront

// ❌ FORBIDDEN: Using Schema.Any to bypass type checking
const config = Schema.Struct({
  settings: Schema.Any, // Define the settings shape!
});
```

**How to fix type-weakened schemas:**

```type

Related in General