# Effect Schema Validation Pattern for Serialization in magnitudedev/magnitude

> Discover the Effect Schema validation pattern in magnitudedev/magnitude for robust runtime type safety and reliable JSON serialization. Learn the six-step approach.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: pattern-explanation
- Published: 2026-09-08

---

**Magnitude uses the Effect Schema library to enforce runtime type safety and reliable JSON serialization through a six-step validation pattern that combines structural schemas, JSON-aware wrappers, and Effect-wrapped I/O operations.**

The magnitudedev/magnitude repository implements a rigorous **Effect Schema validation pattern** to handle all data serialization concerns across its TypeScript codebase. This approach guarantees that every piece of serialized data—from browser local storage to RPC network payloads—undergoes strict validation at runtime while preserving compile-time type safety through schema-derived TypeScript types.

## The Six-Step Effect Schema Validation Pattern

Magnitude applies this pattern uniformly across the codebase to ensure consistent data integrity. Each step serves a distinct purpose in the serialization lifecycle.

### Define Structural Schemas with Schema.Struct

The foundation of the pattern begins by defining the data shape using **Schema.Struct**, **Schema.Literal**, **Schema.Boolean**, or other Effect Schema constructors. In [`web/src/stores/conversation-preferences.ts`](https://github.com/magnitudedev/magnitude/blob/main/web/src/stores/conversation-preferences.ts), the schema is defined as follows:

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

export const ConversationPreferencesSchema = Schema.Struct({
  showThinking: Schema.Boolean,
});

export type ConversationPreferences = typeof ConversationPreferencesSchema.Type;

```

The `.Type` property generates a TypeScript type that stays synchronized with the runtime schema definition, providing compile-time guarantees.

### Create JSON-Aware Schemas with Schema.parseJson

To enable serialization, the structural schema is wrapped with **Schema.parseJson**, creating a schema that can transform between typed values and JSON strings. This step bridges the gap between TypeScript objects and their string representations:

```typescript
const StoredConversationPreferencesSchema = Schema.parseJson(ConversationPreferencesSchema);

```

This wrapper ensures that encoding produces valid JSON and decoding validates the JSON structure against the defined schema.

### Decode Unknown Inputs with Schema.decodeUnknownEither

When reading serialized data from external sources, Magnitude uses **Schema.decodeUnknownEither** to parse and validate the input. This function returns an `Either` type that explicitly handles success and failure cases without throwing exceptions:

```typescript
import { Either } from "effect";

export function decodeConversationPreferences(value: string | null): ConversationPreferences {
  if (value === null) return defaultConversationPreferences;
  const decoded = Schema.decodeUnknownEither(StoredConversationPreferencesSchema)(value);
  return Either.isRight(decoded) ? decoded.right : defaultConversationPreferences;
}

```

This approach treats malformed data as a typed error case rather than a runtime exception, allowing graceful fallback to default values.

### Serialize Data with Schema.encodeSync

For writing data, **Schema.encodeSync** converts typed values back into JSON strings synchronously. This function guarantees that the output conforms to the schema's structure:

```typescript
export function encodeConversationPreferences(value: ConversationPreferences): string {
  return Schema.encodeSync(StoredConversationPreferencesSchema)(value);
}

```

Using `encodeSync` ensures that any value passed to storage APIs has been validated against the schema before serialization.

### Type Errors Using Schema.TaggedError

Magnitude expresses I/O errors as **Schema.TaggedError** (or **Data.TaggedError**) to integrate with Effect's error handling model. These tagged errors enable precise error catching using `Effect.catchTag`:

```typescript
import { Data, Effect } from "effect";

class ConversationPreferenceStorageError extends Data.TaggedError(
  "ConversationPreferenceStorageError"
)<{ readonly operation: "read" | "write" }> {}

```

This pattern creates distinct error types that carry semantic information about the failure context, making error handling both exhaustive and type-safe.

### Wrap I/O in Effect.tryPromise

All storage operations are wrapped in **Effect.tryPromise** to keep side effects observable and composable within Effect workflows. As implemented in [`web/src/stores/conversation-preferences.ts`](https://github.com/magnitudedev/magnitude/blob/main/web/src/stores/conversation-preferences.ts), the pattern looks like this:

```typescript
function readPreferences(storage: Storage) {
  return Effect.tryPromise({
    try: () => storage.getItem(STORAGE_KEY),
    catch: () => new ConversationPreferenceStorageError({ operation: "read" }),
  }).pipe(Effect.map(decodeConversationPreferences));
}

```

The `catch` constructor transforms any JavaScript exceptions into typed `TaggedError` instances, maintaining the purity of the Effect ecosystem while interfacing with impure browser APIs.

## Implementation Examples from the Magnitude Codebase

The following files demonstrate how the six-step pattern adapts to different serialization contexts throughout the repository.

### Local Storage in conversation-preferences.ts

The file [`web/src/stores/conversation-preferences.ts`](https://github.com/magnitudedev/magnitude/blob/main/web/src/stores/conversation-preferences.ts) showcases the complete pattern implementation for persisting user preferences. It combines schema definition, JSON wrapping, decode/encode helpers, and Effect-wrapped storage operations into a cohesive module that handles `showThinking` boolean preferences.

### Enum Validation in appearance-store.ts

In [`web/src/stores/appearance-store.ts`](https://github.com/magnitudedev/magnitude/blob/main/web/src/stores/appearance-store.ts), Magnitude uses **Schema.Literal** to validate enum-like values (such as theme preferences) through the same decode/encode flow. This demonstrates how the pattern handles simple discriminated unions alongside complex structs.

### Branded IDs in workspace-tabs.ts

The [`web/src/lib/workspace-tabs.ts`](https://github.com/magnitudedev/magnitude/blob/main/web/src/lib/workspace-tabs.ts) file extends the pattern with **Schema.brand** to create nominal typing for workspace identifiers. This prevents accidental mixing of different string types (like tab IDs vs. workspace IDs) while maintaining the same JSON serialization guarantees.

### RPC Transport in subscription-wire.test.ts

Across package boundaries, [`packages/acn-protocol/src/transport/subscription-wire.test.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/acn-protocol/src/transport/subscription-wire.test.ts) applies **Schema.encodeSync** to RPC payloads. This illustrates how the same Effect Schema validation pattern ensures type safety in network communication, reusing schemas defined elsewhere in the monorepo.

## Summary

- **Compile-time type safety** is achieved through the `.Type` property on schema definitions, which generates TypeScript types synchronized with runtime validators.
- **Runtime validation** catches malformed JSON before it enters application logic, preventing partial data corruption.
- **Consistent error handling** uses `TaggedError` subclasses that integrate cleanly with Effect's `catchTag` and `catchTags` combinators.
- **Reusability** allows identical schemas to serve local storage, RPC payloads, and configuration validation without duplication.
- **Side-effect management** isolates impure I/O operations within `Effect.tryPromise` wrappers, maintaining referential transparency throughout the codebase.

## Frequently Asked Questions

### What is the Effect Schema validation pattern used for in Magnitude?

The pattern provides a type-safe pipeline for converting between TypeScript objects and serialized JSON strings while validating data integrity at every boundary. It ensures that data read from browser storage or received via network requests conforms to expected shapes before reaching business logic.

### How does Schema.parseJson enable safe serialization?

**Schema.parseJson** wraps a structural schema to handle the JSON string layer, ensuring that encoded values are valid JSON strings and decoded values are parsed and validated against the underlying schema. This prevents common serialization errors like storing `[object Object]` or parsing malformed JSON without validation.

### What is the difference between Schema.decodeUnknownEither and Schema.encodeSync?

**Schema.decodeUnknownEither** parses an unknown input (like a JSON string) and validates it against the schema, returning an `Either<ParseError, A>` to handle failures explicitly. **Schema.encodeSync** performs the inverse operation, converting a validated TypeScript value back into a JSON string synchronously, assuming the input already satisfies the schema type.

### How does Magnitude handle validation errors in Effect workflows?

Magnitude wraps potential failures in **Data.TaggedError** classes and uses `Effect.tryPromise` to convert imperative exceptions into functional error channels. These tagged errors are then caught and handled using `Effect.catchTag`, allowing specific recovery logic for storage read failures versus write failures while maintaining full type safety throughout the error handling chain.