schema-components 1.10.3 → 1.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/README.md +0 -727
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
package/README.md DELETED
@@ -1,727 +0,0 @@
1
- # schema-components
2
-
3
- React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents.
4
-
5
- Define your data model once. Get presentational views, input fields, and editable forms — no manual wiring.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install schema-components
11
- ```
12
-
13
- Peer dependencies: `zod@^4.0.0`, `react@^18.0.0 || ^19.0.0`.
14
-
15
- ## Quick start
16
-
17
- ```tsx
18
- import { z } from "zod";
19
- import { SchemaComponent } from "schema-components/react/SchemaComponent";
20
-
21
- const userSchema = z.object({
22
- name: z.string().min(1).meta({ description: "Full name" }),
23
- email: z.email().meta({ description: "Email address" }),
24
- role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
25
- active: z.boolean().meta({ description: "Active" }),
26
- });
27
-
28
- function UserCard() {
29
- const [user, setUser] = useState({
30
- name: "Ada Lovelace",
31
- email: "ada@example.com",
32
- role: "admin",
33
- active: true,
34
- });
35
-
36
- return (
37
- <SchemaComponent
38
- schema={userSchema}
39
- value={user}
40
- onChange={setUser}
41
- />
42
- );
43
- }
44
- ```
45
-
46
- Renders every field as an editable input. Add `readOnly` to the component for a read-only view:
47
-
48
- ```tsx
49
- <SchemaComponent schema={userSchema} value={user} readOnly />
50
- ```
51
-
52
- ## How it works
53
-
54
- ```
55
- Zod schema ─── z.toJSONSchema() ──→ JSON Schema ──────────┐
56
-
57
- JSON Schema ─────────────────────────────────────────► JSON Schema ──► walker ──► React
58
-
59
- OpenAPI doc ── extract schemas ───────────────────────────┘
60
- ```
61
-
62
- One walker, one input format. The walker reads standard JSON Schema keywords (Draft 2020-12) — decoupled from Zod's internal API. `z.toJSONSchema()` is lossless: it preserves `readOnly`, `writeOnly`, custom `.meta()` properties, constraints, formats, and defaults.
63
-
64
- `z.fromJSONSchema()` is used **only for validation** — converting JSON Schema / OpenAPI inputs back to Zod when `validate` is true and the original wasn't a Zod schema.
65
-
66
- ## Component editability
67
-
68
- Fields render in one of three states, controlled by `readOnly` and `writeOnly` from three sources:
69
-
70
- | State | Rendering |
71
- |---|---|
72
- | **Presentation** | Read-only display. Formatted text, links, badges. No inputs. |
73
- | **Input** | Empty field. Blank inputs, "Select…" dropdowns, unchecked toggles. |
74
- | **Editable** | Pre-populated input the user can change. |
75
-
76
- ### Three sources, priority order
77
-
78
- 1. **Schema property** (`.meta({ readOnly: true })`) — always wins
79
- 2. **Component props** (`readOnly` / `writeOnly` on `<SchemaComponent>`) — rendering context
80
- 3. **Schema root** (`.meta({ readOnly: true })` on root schema) — fallback default
81
- 4. Neither → Editable
82
-
83
- ### Overriding with `readOnly: false`
84
-
85
- A field override can explicitly opt out of a higher-level `readOnly`:
86
-
87
- ```tsx
88
- <SchemaComponent
89
- schema={userSchema}
90
- value={user}
91
- readOnly // everything presentation
92
- fields={{
93
- address: {
94
- readOnly: false, // address subtree: editable
95
- city: { readOnly: true }, // city: still presentation
96
- },
97
- }}
98
- />
99
- ```
100
-
101
- ## Type-safe field overrides
102
-
103
- The `fields` prop type is inferred from the schema:
104
-
105
- ```tsx
106
- // Zod — full autocomplete
107
- <SchemaComponent
108
- schema={userSchema}
109
- fields={{
110
- name: { readOnly: true }, // ✓ type-safe
111
- address: {
112
- city: { description: "City" }, // ✓ nested, type-safe
113
- },
114
- // nme: { readOnly: true }, // ✗ TypeScript error: unknown key
115
- }}
116
- />
117
-
118
- // JSON Schema as const — full autocomplete
119
- const jsonSchema = {
120
- type: "object" as const,
121
- properties: {
122
- name: { type: "string" as const },
123
- email: { type: "string" as const, format: "email" },
124
- },
125
- required: ["name"],
126
- } as const;
127
-
128
- <SchemaComponent
129
- schema={jsonSchema}
130
- fields={{
131
- name: { readOnly: true }, // ✓ inferred from as const
132
- // nme: { readOnly: true }, // ✗ TypeScript error
133
- }}
134
- />
135
-
136
- // OpenAPI as const + ref — full autocomplete
137
- const spec = {
138
- openapi: "3.1.0",
139
- components: {
140
- schemas: {
141
- User: {
142
- type: "object" as const,
143
- properties: {
144
- id: { type: "string" as const },
145
- name: { type: "string" as const },
146
- },
147
- required: ["id", "name"],
148
- },
149
- },
150
- },
151
- } as const;
152
-
153
- <SchemaComponent
154
- schema={spec}
155
- ref="#/components/schemas/User"
156
- fields={{
157
- id: { readOnly: true }, // ✓ inferred through ref
158
- }}
159
- />
160
- ```
161
-
162
- ## Individual fields
163
-
164
- ```tsx
165
- import { SchemaField } from "schema-components/react/SchemaComponent";
166
-
167
- // Type-safe path — only valid dot-paths accepted
168
- <SchemaField
169
- schema={userSchema}
170
- path="address.city" // ✓ type-safe
171
- // path="address.cty" // ✗ TypeScript error
172
- value={user}
173
- onChange={setUser}
174
- />
175
- ```
176
-
177
- When the schema is a Zod schema or typed `as const`, only valid dot-paths like `"address.city"` are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.
178
-
179
- ## All input formats
180
-
181
- `<SchemaComponent>` auto-detects the input format:
182
-
183
- ```tsx
184
- // Zod schema
185
- <SchemaComponent schema={z.object({ name: z.string() })} value={data} />
186
-
187
- // JSON Schema
188
- <SchemaComponent
189
- schema={{ type: "object", properties: { name: { type: "string" } } }}
190
- value={data}
191
- />
192
-
193
- // OpenAPI document + ref
194
- <SchemaComponent
195
- schema={openApiSpec}
196
- ref="#/components/schemas/User"
197
- value={data}
198
- />
199
- ```
200
-
201
- ## OpenAPI components
202
-
203
- Render API operations with type-safe field overrides:
204
-
205
- ```tsx
206
- import { ApiOperation } from "schema-components/openapi/components";
207
- import type { ApiRequestBodyProps } from "schema-components/openapi/components";
208
-
209
- const petStore = {
210
- openapi: "3.1.0",
211
- paths: {
212
- "/pets": {
213
- post: {
214
- requestBody: {
215
- content: {
216
- "application/json": {
217
- schema: {
218
- type: "object" as const,
219
- properties: {
220
- name: { type: "string" as const },
221
- tag: { type: "string" as const },
222
- },
223
- required: ["name"],
224
- },
225
- },
226
- },
227
- },
228
- responses: { "201": { description: "Created" } },
229
- },
230
- },
231
- },
232
- } as const;
233
-
234
- // Full operation — parameters, request body, responses
235
- <ApiOperation schema={petStore} path="/pets" method="post" />
236
-
237
- // Just the request body with type-safe fields
238
- <ApiRequestBody
239
- schema={petStore}
240
- path="/pets"
241
- method="post"
242
- fields={{
243
- name: { description: "Pet name" }, // ✓ inferred from as const
244
- // nme: { description: "X" }, // ✗ TypeScript error
245
- }}
246
- />
247
-
248
- // Just parameters with type-safe overrides
249
- <ApiParameters
250
- schema={petStore}
251
- path="/pets"
252
- method="get"
253
- overrides={{
254
- limit: { description: "Max results" }, // ✓ inferred parameter names
255
- }}
256
- />
257
-
258
- // Response schema
259
- <ApiResponse schema={petStore} path="/pets" method="get" status="200" />
260
- ```
261
-
262
- ## Theme adapters
263
-
264
- Headless by default (plain HTML). Wrap with a theme adapter for styled components:
265
-
266
- ```tsx
267
- import { SchemaProvider } from "schema-components/react/SchemaComponent";
268
- import { shadcnResolver } from "schema-components/themes/shadcn";
269
-
270
- <SchemaProvider resolver={shadcnResolver}>
271
- <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
272
- </SchemaProvider>
273
- ```
274
-
275
- Write a custom adapter:
276
-
277
- ```tsx
278
- import type { RenderProps, ComponentResolver } from "schema-components/core/renderer";
279
-
280
- const myResolver: ComponentResolver = {
281
- string: (props: RenderProps) => {
282
- if (props.readOnly) return <span>{props.value}</span>;
283
- return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
284
- },
285
- object: (props: RenderProps) => {
286
- // props.renderChild recursively renders each field
287
- return (
288
- <div>
289
- {props.fields && Object.entries(props.fields).map(([key, field]) => (
290
- <div key={key}>
291
- <label>{field.meta.description}</label>
292
- {props.renderChild(field, (props.value as Record<string, unknown>)?.[key], (v) => {
293
- props.onChange({ ...(props.value as object), [key]: v });
294
- })}
295
- </div>
296
- ))}
297
- </div>
298
- );
299
- },
300
- };
301
- ```
302
-
303
- Every render function receives `props.renderChild` for recursive rendering — no need to know about the resolver or rendering context.
304
-
305
- ## Raw HTML
306
-
307
- Render schemas to HTML strings — no React needed. Useful for server-side rendering, email templates, static sites, and non-React environments.
308
-
309
- ```tsx
310
- import { renderToHtml } from "schema-components/html/renderToHtml";
311
-
312
- const userSchema = z.object({
313
- name: z.string().meta({ description: "Name" }),
314
- email: z.email().meta({ description: "Email" }),
315
- role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
316
- });
317
-
318
- // Read-only display
319
- const html = renderToHtml(userSchema, {
320
- value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
321
- readOnly: true,
322
- });
323
- // → <dl class="sc-object">
324
- // <dt class="sc-label">Name</dt><dd class="sc-value"><span class="sc-value">Ada Lovelace</span></dd>
325
- // <dt class="sc-label">Email</dt><dd class="sc-value"><a class="sc-value" href="mailto:ada@example.com">ada@example.com</a></dd>
326
- // <dt class="sc-label">Role</dt><dd class="sc-value"><span class="sc-value">admin</span></dd>
327
- // </dl>
328
-
329
- // Editable form
330
- const formHtml = renderToHtml(userSchema, {
331
- value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
332
- });
333
- // → <fieldset class="sc-object">
334
- // <div class="sc-field">
335
- // <label class="sc-label" for="sc-name">Name</label>
336
- // <input class="sc-input" type="text" name="" value="Ada Lovelace">
337
- // </div>
338
- // ...
339
- // </fieldset>
340
- ```
341
-
342
- All HTML output uses `sc-` prefixed classes for styling hooks. HTML is properly escaped by the serialiser — no manual escaping needed.
343
-
344
- A default stylesheet is included:
345
-
346
- ```html
347
- <link rel="stylesheet" href="node_modules/schema-components/dist/html/styles.css">
348
- ```
349
-
350
- Or import in JS:
351
-
352
- ```ts
353
- import "schema-components/styles.css";
354
- ```
355
-
356
- ### Structured HTML construction
357
-
358
- The HTML renderer uses a typed `h()` builder instead of string templates. This gives compile-time safety and automatic escaping:
359
-
360
- ```ts
361
- import { h, serialize, raw } from "schema-components/html/html";
362
-
363
- // Build elements — attrs are type-checked, values auto-escaped
364
- const input = h("input", { type: "text", id: "name", value: userValue });
365
- serialize(input); // → <input type="text" id="name" value="Ada">
366
-
367
- // Embed pre-serialised HTML (from child renderers)
368
- const div = h("div", { class: "field" }, raw(childHtml));
369
- serialize(div);
370
- ```
371
-
372
- The builder handles void elements (`<input>`, `<br>`, etc.), boolean attributes (`checked`, `disabled`), fragments, and nested children.
373
-
374
- ### Streaming HTML
375
-
376
- Three output formats for incremental rendering:
377
-
378
- ```ts
379
- import { renderToHtmlChunks } from "schema-components/html/renderToHtmlStream";
380
- import { renderToHtmlStream } from "schema-components/html/renderToHtmlStream";
381
- import { renderToHtmlReadable } from "schema-components/html/renderToHtmlStream";
382
-
383
- // Sync iterable — chunks yielded at field/item/entry boundaries
384
- const chunks: string[] = [...renderToHtmlChunks(schema, { value })];
385
-
386
- // Async iterable — yields control to event loop between chunks
387
- for await (const chunk of renderToHtmlStream(schema, { value })) {
388
- res.write(chunk);
389
- }
390
-
391
- // Web ReadableStream — for Response, TransformStream, etc.
392
- return new Response(renderToHtmlReadable(schema, { value }), {
393
- headers: { "Content-Type": "text/html" },
394
- });
395
- ```
396
-
397
- Concatenating all chunks produces identical output to `renderToHtml`.
398
-
399
- ### Accessibility
400
-
401
- The HTML renderer produces WAI-ARIA-compliant markup:
402
-
403
- | Attribute | When |
404
- |---|---|
405
- | `id="<key>"` | All editable inputs |
406
- | `aria-required="true"` | Required fields (`isOptional === false`) |
407
- | `aria-describedby="<id>-hint"` | Fields with constraints (min/max/length/pattern) |
408
- | `aria-readonly="true"` | Read-only presentation spans |
409
- | `aria-label="<description>"` | Checkboxes (no visible text node) |
410
- | `role="group"` | Record containers |
411
- | `aria-label` on `<fieldset>` | Object with description |
412
- | `<small class="sc-hint">` | Constraint hint text |
413
- | `<span class="sc-required" aria-hidden="true">*` | Required field indicator |
414
-
415
- ### Custom HTML resolver
416
-
417
- ```ts
418
- import { renderToHtml } from "schema-components/html/renderToHtml";
419
- import type { HtmlResolver, HtmlRenderProps } from "schema-components/html/renderToHtml";
420
-
421
- const tailwindResolver: HtmlResolver = {
422
- string: (props: HtmlRenderProps) => {
423
- if (props.readOnly) {
424
- return `<span class="text-sm text-gray-700">${typeof props.value === "string" ? props.value : ""}</span>`;
425
- }
426
- return `<input class="border rounded px-2 py-1" type="text" value="${typeof props.value === "string" ? props.value : "">">`;
427
- },
428
- };
429
-
430
- const html = renderToHtml(schema, { value, readOnly: true, resolver: tailwindResolver });
431
- ```
432
-
433
- Custom resolvers fall back to the default for any type you don't override.
434
-
435
- ## Custom widgets
436
-
437
- Widgets let you override rendering for specific fields using `.meta({ component: name })`. Three scopes are available, checked in order:
438
-
439
- 1. **Instance** — `widgets` prop on `<SchemaComponent>`
440
- 2. **Context** — `widgets` prop on `<SchemaProvider>`
441
- 3. **Global** — `registerWidget()` for app-wide defaults
442
-
443
- If none match, the theme adapter or headless default handles the field.
444
-
445
- ### Global registration
446
-
447
- ```tsx
448
- import { registerWidget } from "schema-components/react/SchemaComponent";
449
-
450
- registerWidget("richtext", ({ value, onChange }) => (
451
- <RichTextEditor value={value} onChange={onChange} />
452
- ));
453
-
454
- const schema = z.object({
455
- bio: z.string().meta({ component: "richtext" }),
456
- });
457
- ```
458
-
459
- ### Context-scoped widgets
460
-
461
- Share widgets across a subtree via `<SchemaProvider>`:
462
-
463
- ```tsx
464
- import { SchemaProvider } from "schema-components/react/SchemaComponent";
465
- import type { WidgetMap } from "schema-components/react/SchemaComponent";
466
-
467
- const adminWidgets: WidgetMap = new Map([
468
- ["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
469
- ["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
470
- ]);
471
-
472
- <SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
473
- <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
474
- <SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
475
- </SchemaProvider>
476
- ```
477
-
478
- ### Instance-scoped widgets
479
-
480
- Override widgets for a single form:
481
-
482
- ```tsx
483
- const formWidgets: WidgetMap = new Map([
484
- ["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
485
- ]);
486
-
487
- <SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />
488
- ```
489
-
490
- ### Resolution order
491
-
492
- ```.meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default
493
- ```
494
-
495
- Instance overrides context. Context overrides global. Unhinted fields skip the widget layer entirely.
496
-
497
- ### `WidgetMap` type
498
-
499
- ```tsx
500
- import type { WidgetMap } from "schema-components/react/SchemaComponent";
501
-
502
- // ReadonlyMap<string, (props: RenderProps) => unknown>
503
- const widgets: WidgetMap = new Map([
504
- ["name", (props) => <MyInput {...props} />],
505
- ]);
506
- ```
507
-
508
- Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React context available):
509
-
510
- ```tsx
511
- <SchemaView schema={schema} value={data} widgets={serverWidgets} />
512
- ```
513
-
514
- ## Validation
515
-
516
- ```tsx
517
- <SchemaComponent
518
- schema={userSchema}
519
- value={user}
520
- onChange={setUser}
521
- validate
522
- onValidationError={(error) => console.error(error)}
523
- />
524
- ```
525
-
526
- Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
527
-
528
- ### Per-field validation errors
529
-
530
- Add `onValidationError` to individual field overrides to receive errors for specific fields:
531
-
532
- ```tsx
533
- <SchemaComponent
534
- schema={userSchema}
535
- value={user}
536
- onChange={setUser}
537
- validate
538
- fields={{
539
- email: { onValidationError: (err) => setEmailError(err) },
540
- name: { onValidationError: (err) => setNameError(err) },
541
- }}
542
- />
543
- ```
544
-
545
- Errors are dispatched based on Zod error paths. The root-level `onValidationError` still receives all errors.
546
-
547
- ## Discriminated unions
548
-
549
- Discriminated unions (`z.discriminatedUnion` or JSON Schema `oneOf` with `const` properties) render as tabbed panels. Each tab is labelled by the discriminator's `const` value. Clicking a tab resets the value with the new discriminator.
550
-
551
- ```tsx
552
- const payment = z.discriminatedUnion("method", [
553
- z.object({
554
- method: z.literal("card"),
555
- cardNumber: z.string(),
556
- expiry: z.string(),
557
- }),
558
- z.object({
559
- method: z.literal("bank"),
560
- accountNumber: z.string(),
561
- sortCode: z.string(),
562
- }),
563
- ]);
564
-
565
- <SchemaComponent schema={payment} value={{ method: "card", cardNumber: "4111...", expiry: "12/28" }} />
566
- ```
567
-
568
- In read-only mode, only the active variant is rendered (no tabs).
569
-
570
- ## Date and time inputs
571
-
572
- String schemas with `format: "date"`, `format: "time"`, or `format: "date-time"` render as the corresponding HTML5 input types:
573
-
574
- ```tsx
575
- const eventSchema = z.object({
576
- date: z.string().meta({ format: "date" }),
577
- startTime: z.string().meta({ format: "time" }),
578
- createdAt: z.string().meta({ format: "date-time" }),
579
- });
580
- ```
581
-
582
- This produces `<input type="date">`, `<input type="time">`, and `<input type="datetime-local">` respectively. In read-only mode, dates are formatted using `toLocaleDateString()` / `toLocaleString()`.
583
-
584
- ## Schema defaults
585
-
586
- Default values from `z.string().default("hello")` or JSON Schema `"default": "hello"` are used when the `value` prop is `undefined`:
587
-
588
- ```tsx
589
- const schema = z.object({
590
- name: z.string().default("World"),
591
- count: z.number().default(0),
592
- });
593
-
594
- // Renders with "World" and 0 pre-filled
595
- <SchemaComponent schema={schema} />
596
- ```
597
-
598
- Defaults propagate through nested objects — each field uses its own default independently.
599
-
600
- ## Field visibility
601
-
602
- Hide fields conditionally using the `visible` override:
603
-
604
- ```tsx
605
- <SchemaComponent
606
- schema={paymentSchema}
607
- value={payment}
608
- fields={{
609
- cardNumber: { visible: payment.method === "card" },
610
- sortCode: { visible: payment.method === "bank" },
611
- }}
612
- />
613
- ```
614
-
615
- When `visible: false`, the field is completely removed — no label, no empty placeholder, no hidden input. The parent component controls visibility based on the current value.
616
-
617
- ## Field ordering
618
-
619
- Control the order fields appear in rendered objects using `order`:
620
-
621
- ```tsx
622
- <SchemaComponent
623
- schema={userSchema}
624
- value={user}
625
- fields={{
626
- email: { order: 1 },
627
- name: { order: 2 },
628
- role: { order: 3 },
629
- }}
630
- />
631
- ```
632
-
633
- Lower `order` values render first. Fields without `order` keep their insertion order and appear after ordered fields. Can also be set in schema metadata:
634
-
635
- ```tsx
636
- const schema = z.object({
637
- summary: z.string().meta({ order: 1 }),
638
- title: z.string().meta({ order: 2 }),
639
- });
640
- ```
641
-
642
- ## Server Components
643
-
644
- For read-only rendering in a React Server Component, use `<SchemaView>`. It has zero hooks — no `useContext`, no `useMemo`, no `useCallback` — so it works without the `"use client"` directive.
645
-
646
- ```tsx
647
- import { SchemaView } from "schema-components/react/SchemaView";
648
-
649
- export default async function Page() {
650
- const user = await getUser();
651
- return <SchemaView schema={userSchema} value={user} />;
652
- }
653
- ```
654
-
655
- `SchemaView` always renders read-only. For editable forms, use `<SchemaComponent>` (which requires `"use client"`).
656
-
657
- Pass the resolver explicitly since React context is unavailable in Server Components:
658
-
659
- ```tsx
660
- import { SchemaView } from "schema-components/react/SchemaView";
661
- import { shadcnResolver } from "schema-components/themes/shadcn";
662
-
663
- <SchemaView schema={schema} value={data} resolver={shadcnResolver} />
664
- ```
665
-
666
- `SchemaView` produces identical output to `<SchemaComponent readOnly>` — verified by parity tests.
667
-
668
- ## File uploads
669
-
670
- String schemas with `format: "binary"` render as `<input type="file">`. Use `contentMediaType` to restrict accepted MIME types:
671
-
672
- ```tsx
673
- const schema = z.object({
674
- avatar: z.string().meta({ format: "binary" }),
675
- resume: z.string().meta({ format: "binary", contentMediaType: "application/pdf" }),
676
- });
677
- ```
678
-
679
- In read-only mode, file fields display a static label ("File field") since there is no value to show. The `onChange` callback receives the `File` object from the browser.
680
-
681
- ## Error handling
682
-
683
- Typed errors with `onError` callback for graceful degradation:
684
-
685
- ```tsx
686
- import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";
687
- import { SchemaComponent } from "schema-components/react/SchemaComponent";
688
-
689
- // Error boundary catches render errors from theme adapters
690
- <SchemaErrorBoundary fallback={(error, reset) => <p>Error: {error.message}</p>}>
691
- <SchemaComponent schema={schema} value={data} />
692
- </SchemaErrorBoundary>
693
-
694
- // Per-component error callback
695
- <SchemaComponent
696
- schema={schema}
697
- value={data}
698
- onError={(error) => {
699
- console.error(error);
700
- return null; // graceful degradation
701
- }}
702
- />
703
- ```
704
-
705
- Without `onError`, errors re-throw. Error hierarchy: `SchemaError` → `SchemaNormalisationError` | `SchemaRenderError` | `SchemaFieldError`.
706
-
707
- ## Architecture
708
-
709
- ```
710
- schema-components
711
- ├── core # JSON Schema walker, ComponentResolver, RenderProps, typed errors, type guards
712
- ├── react # SchemaComponent ("use client"), SchemaView (server component), headless renderer, error boundary
713
- ├── openapi # Document parser, ApiOperation, ApiParameters, ApiRequestBody, ApiResponse
714
- ├── html # h() builder, renderToHtml, streaming renderers, ARIA helpers
715
- └── themes # shadcn, MUI, custom adapters (separate packages)
716
- ```
717
-
718
- Every module is imported directly — no barrel files. Organised exports:
719
-
720
- ```
721
- schema-components/core/* # Walker, types, guards, errors, resolver
722
- schema-components/react/* # SchemaComponent, SchemaView, SchemaErrorBoundary, headless
723
- schema-components/openapi/* # Parser, ApiOperation, ApiParameters, etc.
724
- schema-components/html/* # renderToHtml, renderToHtmlChunks, h() builder, styles
725
- schema-components/themes/* # shadcn, MUI, custom adapters
726
- schema-components/styles.css # Default stylesheet for HTML output
727
- ```