Effect Schema `optionalWith` for Safe JSON Serialization in Magnitude: A Complete Guide
Effect Schema's optionalWith with { as: "Option", exact: true } is the canonical way to handle optional values in Magnitude, ensuring undefined never leaks into JSON by wrapping values in Option and omitting None fields entirely during encoding.
Magnitude uses Effect Schema to model all data structures that cross system boundaries—API payloads, persisted session state, and CLI tool configurations. The primary challenge is JavaScript's undefined, which serializes to invalid JSON. This article explains how Magnitude enforces safe optional serialization through Schema.optionalWith and the compile-time IsSchemaJsonSafe type guard.
The Problem with undefined in JSON
JSON has no representation for undefined. When JSON.stringify() encounters undefined as an object property value, it either omits the key or converts it to null—behavior that destroys type safety and causes unexpected bugs in downstream systems.
Effect Schema provides two approaches for optional fields, with vastly different serialization guarantees:
| Declaration | Decoded Type | JSON Safety |
|---|---|---|
Schema.optional(X) / Schema.UndefinedOr(X) |
X | undefined |
Unsafe — undefined may escape during encoding |
Schema.optionalWith(X, { as: "Option", exact: true }) |
Option<X> |
Safe — None omits the key; Some(v) emits v |
Magnitude's codebase exclusively uses the second pattern for any schema that will be serialized to JSON.
How optionalWith Works: Encoding Behavior
When you declare a field with Schema.optionalWith(Schema.String, { as: "Option", exact: true }), the resulting TypeScript type is Option<string>. During encoding via Schema.encodeSync(), the transformation is deterministic:
Some(value)→ the property is included withvalueNone→ the property is entirely omitted from the output object
import { Schema, Option } from 'effect';
// Safe schema: email is Option<string>
const UserSchema = Schema.Struct({
id: Schema.String,
email: Schema.optionalWith(Schema.String, { as: 'Option', exact: true })
});
// Encoding examples
const withEmail = Schema.encodeSync(UserSchema)({
id: '123',
email: Option.some('alice@example.com')
});
// → { id: "123", email: "alice@example.com" }
const withoutEmail = Schema.encodeSync(UserSchema)({
id: '124',
email: Option.none()
});
// → { id: "124" } // email key omitted — no undefined
This behavior is implemented in Effect Schema's core encoding logic, which Magnitude consumes.
Compile-Time Enforcement with IsSchemaJsonSafe
Magnitude guarantees JSON safety through a custom type guard in packages/utils/src/schema/json-safe.ts. The IsSchemaJsonSafe<S> utility recursively inspects any schema at compile time and rejects schemas that could produce undefined.
Detection Logic
The guard operates in three phases (see lines 13–27 and 123 in json-safe.ts):
IsFieldUnsafechecks individual properties: if a field's type includesundefinedand is not wrapped inOption, it's flagged as unsafe- Recursive traversal inspects nested structs, unions, arrays, and transformation nodes (
from) - Boolean result:
IsSchemaJsonSafe<S>evaluates totrueonly when all branches are safe
If you accidentally use Schema.optional, the type checker produces a clear error:
__jsonSafeError: Schema contains bare Schema.optional() or Schema.UndefinedOr()
This error originates from the type definition at line 123 of json-safe.ts, forcing immediate correction.
Example: Unsafe vs. Safe Declarations
// ❌ UNSAFE: triggers compile-time error in Magnitude
const BadSchema = Schema.Struct({
id: Schema.String,
phone: Schema.optional(Schema.String) // Schema.optional: X | undefined
});
// Type error: __jsonSafeError...
// ❌ ALSO UNSAFE: UndefinedOr has same problem
const AlsoBad = Schema.Struct({
id: Schema.String,
phone: Schema.UndefinedOr(Schema.String)
});
// ✅ SAFE: Option wrapper with exact: true
const GoodSchema = Schema.Struct({
id: Schema.String,
phone: Schema.optionalWith(Schema.String, { as: 'Option', exact: true })
});
// Compiles: IsSchemaJsonSafe evaluates to true
Real-World Usage in Magnitude
The Magnitude codebase demonstrates optionalWith patterns across multiple packages.
CLI Tool Arguments (packages/vcs/src/tools.ts)
Optional CLI parameters like since, glob, and diff use optionalWith to represent arguments that may be omitted:
// Pattern from tools.ts — optional string parameters
since: Schema.optionalWith(Schema.String, { as: 'Option', exact: true }),
glob: Schema.optionalWith(Schema.String, { as: 'Option', exact: true }),
Session State (packages/storage/src/types/session.ts)
Persisted UI state uses Option for optional fields like archived and sidebarOpen:
// From session.ts — safely serialized to storage
archived: Schema.optionalWith(Schema.Boolean, { as: 'Option', exact: true }),
sidebarOpen: Schema.optionalWith(Schema.Boolean, { as: 'Option', exact: true }),
These fields may be None if the user hasn't toggled the relevant UI element, and the storage layer safely omits them without undefined pollution.
Provider Data (packages/providers/src/magnitude/provider.ts)
Provider-specific extensions use the same pattern for optional nested data:
// Pattern from provider.ts — optional provider-specific payload
data: Schema.optionalWith(ProviderDataSchema, { as: 'Option', exact: true }),
Test Coverage (packages/utils/src/patch/__tests__/exhaustive.test.ts)
The test suite in exhaustive.test.ts validates that nested schemas with optionalWith fields correctly handle Some and None cases through multiple encoding/decoding round-trips.
Working with Option Values
To construct and consume Option values in your Magnitude code:
import { Option, pipe } from 'effect';
// Creating values
const someValue = Option.some("present");
const noValue = Option.none();
// Pattern matching
const result = pipe(
someValue,
Option.match({
onSome: (v) => `Value: ${v}`,
onNone: () => "No value"
})
);
// Converting from nullable APIs
const fromNullable = Option.fromNullable(maybeString); // null/undefined → None
The Option type integrates with Effect's full ecosystem—Effect.gen, pipe, and Option.match provide ergonomic handling without risking undefined propagation.
Summary
Schema.optionalWith(X, { as: "Option", exact: true })is Magnitude's required pattern for optional JSON-serializable fieldsOption<X>encodes to either the wrapped value (Some) or complete key omission (None)Schema.optionalandSchema.UndefinedOrare compile-time rejected byIsSchemaJsonSafeinpackages/utils/src/schema/json-safe.ts- The guard recursively validates structs, unions, arrays, and transformations at compile time
- Real usage appears in
packages/vcs/src/tools.ts,packages/storage/src/types/session.ts, andpackages/providers/src/magnitude/provider.ts
Frequently Asked Questions
What happens if I forget exact: true in optionalWith?
Without exact: true, optionalWith may produce different semantics depending on the Effect Schema version. Magnitude's IsSchemaJsonSafe guard requires the exact option to ensure consistent behavior. Omitting it can cause the type check to fail or produce unexpected encoding results.
Can I use Option from fp-ts or another library instead?
No. Magnitude's IsSchemaJsonSafe specifically recognizes Effect's native Option type. Other Option implementations would be treated as unknown objects and likely fail the safety check. Import Option from effect as shown in the code examples.
How do I migrate existing Schema.optional schemas to the safe pattern?
Replace Schema.optional(X) with Schema.optionalWith(X, { as: 'Option', exact: true }), then update your value construction from { field: undefined } or omitted keys to Option.none() or Option.some(value). The type checker will guide remaining fixes through __jsonSafeError diagnostics.
Does IsSchemaJsonSafe impact runtime performance?
No. The check is purely compile-time TypeScript type manipulation. It produces no runtime code—only type-level boolean evaluation. Your encoded schemas have zero overhead from the safety verification.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →