Frontend Schema Validation API
Effect Schema validation system for type-safe data validation across all layers.
Schema Architecture
Strategic validation boundaries using Effect Schema for input validation, data transformation, and type safety.
Base Schema Patterns
import { Schema } from "@effect/schema";
// Entity schemas
export const EntityStatusSchema = Schema.Union(
Schema.Literal("pending"),
Schema.Literal("approved"),
Schema.Literal("rejected"),
);
export const BaseEntitySchema = Schema.Struct({
name: Schema.String,
description: Schema.String,
status: EntityStatusSchema,
});
// Input schemas with validation
export const CreateEntityInputSchema = Schema.Struct({
name: Schema.String.pipe(Schema.minLength(1)),
description: Schema.String.pipe(Schema.minLength(1)),
});
// UI entity schemas (includes UI-specific fields)
export const UIEntitySchema = BaseEntitySchema.extend(
Schema.Struct({
hash: Schema.String, // ActionHash
createdAt: Schema.Date,
}),
);
Domain Schemas
Service Type Schemas
File: ui/src/lib/schemas/service-type.schemas.ts
Complete schema definitions for service type validation and transformation.
Request Schemas
File: ui/src/lib/schemas/request.schemas.ts
Request-specific schemas with proper validation boundaries.
User Schemas
File: ui/src/lib/schemas/users.schemas.ts
User profile schemas, plus the form-collection split that separates the DHT name field into given and family name inputs. UserInDHT is unchanged — name remains a single string — so there is no DNA bump and no data migration. The split lives only at the form boundary.
// Form-collection shape: name split into two required fields.
// Mononymous users enter "." in family_name as an explicit declaration.
export const UserFormInputSchema = S.Struct({
given_name: S.String.pipe(S.minLength(1), S.maxLength(100)),
family_name: S.String.pipe(S.minLength(1), S.maxLength(100)),
nickname: S.String.pipe(S.minLength(1), S.maxLength(50)),
// ...remaining fields identical to UserInDHTSchema
});
/** Form → DHT: joins the two fields with a single space. Mononyms become "Sting ." */
export const formInputToDHT = (input: UserFormInput): UserInDHT => /* ... */;
/** DHT → form, for edit-mode pre-fill: first-space split, so compound family
* names ("del Carmen Rodriguez") are preserved. Mononyms get an empty
* family_name rather than an inherited dot. */
export const dhtToFormInput = (user: UserInDHT): UserFormInput => /* ... */;
/** Display helper: strips the " ." sentinel from a stored name.
* Embedded dots ("Dr. Smith", "J. R. R. Tolkien") are preserved. */
export const formatUserName = (name: string | null | undefined): string => /* ... */;
The trailing dot in a stored mononym ("Sting .") is a vetting marker only; formatUserName strips it at every display site so it never reaches the UI. The action hash on a user record remains the canonical identity reference — names are display labels.
Common Schemas
File: ui/src/lib/schemas/common.schemas.ts
// Shared schemas used across domains
export const TimePreferenceSchema = Schema.Union(
Schema.Literal("asap"),
Schema.Literal("flexible"),
Schema.Literal("specific"),
);
export const InteractionTypeSchema = Schema.Union(
Schema.Literal("in-person"),
Schema.Literal("remote"),
Schema.Literal("hybrid"),
);
Branded Hash Types
File: ui/src/lib/schemas/holochain.schemas.ts
Compile-time distinct types that prevent accidental swapping of original and previous action hashes:
// Branded action hash types — zero runtime cost
export type OriginalActionHash = ActionHash & { readonly __brand: 'OriginalActionHash' };
export type PreviousActionHash = ActionHash & { readonly __brand: 'PreviousActionHash' };
/** Cast an ActionHash as an OriginalActionHash (zero-cost, compile-time only) */
export const asOriginalActionHash = (hash: ActionHash): OriginalActionHash =>
hash as OriginalActionHash;
/** Cast an ActionHash as a PreviousActionHash (zero-cost, compile-time only) */
export const asPreviousActionHash = (hash: ActionHash): PreviousActionHash =>
hash as PreviousActionHash;
These types correspond to Rust newtypes OriginalActionHash and PreviousActionHash in dnas/requests_and_offers/utils/src/types.rs. The #[serde(transparent)] attribute on the Rust side ensures wire format compatibility — no migration needed.
See Action Hash Type Safety for the full specification.
Validation Integration
Service Layer Validation
Schemas integrate with services for input/output validation and type safety.
Store Layer Validation
Strategic validation at store boundaries for data integrity.
This schema system provides comprehensive type safety and validation across the application.