# 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.
