@payfit/unity-components
Version:
109 lines (75 loc) • 4.09 kB
Markdown
# Schema adapters
Schema adapters give the form-field organisms a uniform way to ask "is this path required?" across schemas authored in Zod 3, Zod 4, or any Standard Schema v1 implementation. Source: `src/adapters/`.
## Common interface
```ts
// src/types/schema.ts
export interface StandardSchemaField {
isOptional: boolean
type: string
shape?: Record<string, StandardSchemaField>
}
export interface StandardSchema {
getField(path: string): StandardSchemaField | null
}
```
`getField('preferences.marketing')` returns `null` if the path does not exist, otherwise `{ isOptional, type, shape }`. `shape` is only populated when the field resolves to a nested `ZodObject` (so callers can recurse into nested forms).
## Adapters
### ZodV3SchemaAdapter
Signature:
```ts
new ZodV3SchemaAdapter(schema: z3.ZodObject<z3.ZodRawShape>)
```
- Reads structure from `schema.shape` and `field._def.typeName`.
- Detects optional with `field instanceof z3.ZodOptional`; unwraps via `field._def.innerType`.
- Source: `src/adapters/zodAdapter.ts` (lines 6–60).
### ZodV4SchemaAdapter
Signature:
```ts
new ZodV4SchemaAdapter(schema: z4.ZodObject<z4.ZodRawShape>)
```
- Same shape traversal as v3, but uses `field.def.innerType` and `field.def.typeName` (no underscore — Zod 4 renamed `_def` to `def`).
- `field instanceof z4.ZodOptional` for optionality detection.
- Source: `src/adapters/zodAdapter.ts` (lines 62–117).
### StandardSchemaAdapter
Signature:
```ts
new StandardSchemaAdapter(standardSchema: StandardSchemaV1)
```
- Stub implementation: `getField()` returns `{ isOptional: false, type: 'unknown', shape: undefined }` for any non-null path. Standard Schema's spec does not expose enough internal structure for richer introspection.
- Use this only for schemas that aren't Zod (e.g. Valibot, ArkType) — required-field inference will degrade to "always required".
- Source: `src/adapters/standardSchemaAdapter.ts`.
## How adapters auto-select
`createSchemaAdapter(schema)` (in `src/utils/createSchemaAdapter.ts`) picks an adapter from the schema's structural fingerprint:
```ts
import { createSchemaAdapter } from '@payfit/unity-components'
const adapter = createSchemaAdapter(schema)
// schema._def && 'def' in schema → ZodV4SchemaAdapter
// schema._def && !('def' in schema) → ZodV3SchemaAdapter
// schema['~validate'] is a function → StandardSchemaAdapter
// otherwise → null
```
You rarely call this directly: every Composed field organism (e.g. `TanstackTextField`, `TanstackCheckboxField`, `TanstackToggleSwitchGroupField`) calls `createSchemaAdapter` + `isFieldRequired` internally to render the required indicator on the label.
## isFieldRequired
Consumer of the adapter, used inside every Composed field to decide whether to mark the label as required.
```ts
// src/components/form-field/utils/isFieldRequired.ts
export function isFieldRequired(
schema: StandardSchema | null | undefined,
fieldPath: string,
): boolean {
if (!schema) return false
const field = schema.getField(fieldPath)
return field ? !field.isOptional : false
}
```
Behavior:
- No schema → `false` (no required indicator).
- Path not found in schema → `false` (treat as not required rather than crashing).
- Field is `ZodOptional` → `false`.
- Field is anything else → `true`.
This is why `<form.AppField name="email">{field => <field.TextField label="Email" />}</form.AppField>` automatically gets a required asterisk when `email` is `z.email()` and no asterisk when it's `z.email().optional()` — without any prop wiring.
## When to import an adapter explicitly
Almost never. The Composed field components call `createSchemaAdapter(schema)` themselves. Import an adapter directly only when:
- You are writing a custom field component outside the Composed inventory and need required-state inference.
- You are introspecting a schema for non-form purposes (form-derived UI, dynamic field rendering).
Even then, prefer `createSchemaAdapter(schema)` so the right version is picked at runtime.