0
# experimental_useObject Hook
1
2
Streams structured objects validated with Zod schemas. Perfect for forms, data extraction, and structured content generation.
3
4
```typescript
5
import { experimental_useObject } from '@ai-sdk/react';
6
import { z } from 'zod';
7
```
8
9
## API
10
11
```typescript { .api }
12
function experimental_useObject<
13
SCHEMA extends ZodType | Schema,
14
RESULT = InferSchema<SCHEMA>,
15
INPUT = any
16
>(
17
options: Experimental_UseObjectOptions<SCHEMA, RESULT>
18
): Experimental_UseObjectHelpers<RESULT, INPUT>;
19
20
interface Experimental_UseObjectOptions<SCHEMA, RESULT> {
21
api: string; // Required
22
schema: SCHEMA; // Required
23
id?: string;
24
initialValue?: DeepPartial<RESULT>;
25
fetch?: FetchFunction;
26
onFinish?: (event: { object: RESULT | undefined; error: Error | undefined }) => Promise<void> | void;
27
onError?: (error: Error) => void;
28
headers?: Record<string, string> | Headers;
29
credentials?: RequestCredentials;
30
}
31
32
interface Experimental_UseObjectHelpers<RESULT, INPUT> {
33
submit: (input: INPUT) => void;
34
object: DeepPartial<RESULT> | undefined;
35
error: Error | undefined;
36
isLoading: boolean;
37
stop: () => void;
38
clear: () => void;
39
}
40
```
41
42
## Basic Usage
43
44
```typescript
45
import { experimental_useObject } from '@ai-sdk/react';
46
import { z } from 'zod';
47
48
const schema = z.object({
49
title: z.string(),
50
description: z.string(),
51
tags: z.array(z.string()),
52
published: z.boolean(),
53
});
54
55
type Article = z.infer<typeof schema>;
56
57
function ArticleGenerator() {
58
const { object, submit, isLoading, error } = experimental_useObject({
59
api: '/api/generate-article',
60
schema,
61
onFinish: ({ object, error }) => {
62
if (error) console.error('Validation error:', error);
63
else console.log('Article generated:', object);
64
},
65
});
66
67
return (
68
<div>
69
<button onClick={() => submit({ topic: 'AI' })} disabled={isLoading}>
70
Generate Article
71
</button>
72
73
{error && <div className="error">{error.message}</div>}
74
75
{object && (
76
<div>
77
<h2>{object.title || 'Loading title...'}</h2>
78
<p>{object.description || 'Loading description...'}</p>
79
<div>
80
{object.tags?.map((tag, i) => <span key={i}>{tag}</span>)}
81
</div>
82
{object.published !== undefined && (
83
<span>{object.published ? 'Published' : 'Draft'}</span>
84
)}
85
</div>
86
)}
87
</div>
88
);
89
}
90
```
91
92
## Production Patterns
93
94
### Validation Error Recovery
95
96
```typescript
97
import { experimental_useObject } from '@ai-sdk/react';
98
import { z } from 'zod';
99
100
const userSchema = z.object({
101
name: z.string().min(1, 'Name required'),
102
email: z.string().email('Invalid email'),
103
age: z.number().min(18, 'Must be 18+').max(120, 'Invalid age'),
104
});
105
106
type User = z.infer<typeof userSchema>;
107
108
function UserGenerator() {
109
const [validationErrors, setValidationErrors] = useState<string[]>([]);
110
111
const { object, submit, error, isLoading, clear } = experimental_useObject({
112
api: '/api/generate-user',
113
schema: userSchema,
114
onFinish: ({ object, error }) => {
115
if (error) {
116
// Parse Zod validation errors
117
try {
118
const zodError = JSON.parse(error.message);
119
setValidationErrors(zodError.errors?.map((e: any) => e.message) || [error.message]);
120
} catch {
121
setValidationErrors([error.message]);
122
}
123
} else {
124
setValidationErrors([]);
125
console.log('Valid user:', object);
126
}
127
},
128
onError: (error) => {
129
console.error('Request error:', error);
130
setValidationErrors([error.message]);
131
},
132
});
133
134
const handleRetry = () => {
135
setValidationErrors([]);
136
clear();
137
submit({ retry: true });
138
};
139
140
return (
141
<div>
142
<button onClick={() => submit({ prompt: 'Generate user' })} disabled={isLoading}>
143
Generate
144
</button>
145
146
{validationErrors.length > 0 && (
147
<div className="validation-errors">
148
<h4>Validation Errors:</h4>
149
<ul>
150
{validationErrors.map((err, i) => (
151
<li key={i}>{err}</li>
152
))}
153
</ul>
154
<button onClick={handleRetry}>Retry</button>
155
</div>
156
)}
157
158
{error && <div className="error">Error: {error.message}</div>}
159
160
{object && (
161
<div className="user-profile">
162
<p>Name: {object.name || 'Loading...'}</p>
163
<p>Email: {object.email || 'Loading...'}</p>
164
<p>Age: {object.age ?? 'Loading...'}</p>
165
</div>
166
)}
167
</div>
168
);
169
}
170
```
171
172
### Progressive Form Generation
173
174
```typescript
175
import { experimental_useObject } from '@ai-sdk/react';
176
import { z } from 'zod';
177
178
const formSchema = z.object({
179
title: z.string(),
180
fields: z.array(
181
z.object({
182
name: z.string(),
183
label: z.string(),
184
type: z.enum(['text', 'email', 'number', 'textarea', 'select', 'checkbox']),
185
required: z.boolean(),
186
placeholder: z.string().optional(),
187
options: z.array(z.string()).optional(),
188
defaultValue: z.string().optional(),
189
})
190
),
191
submitButton: z.string(),
192
});
193
194
type FormDefinition = z.infer<typeof formSchema>;
195
196
function AIFormGenerator() {
197
const { object, submit, isLoading, clear, stop } = experimental_useObject({
198
api: '/api/generate-form',
199
schema: formSchema,
200
experimental_throttle: 50,
201
});
202
203
const [formData, setFormData] = useState<Record<string, any>>({});
204
const isComplete = !isLoading && object && object.fields && object.fields.length > 0;
205
206
const handleFormSubmit = (e: React.FormEvent) => {
207
e.preventDefault();
208
console.log('Form submitted:', formData);
209
};
210
211
const renderField = (field: FormDefinition['fields'][0], index: number) => {
212
if (!field.name) return <div key={index}>Loading field...</div>;
213
214
const commonProps = {
215
name: field.name,
216
placeholder: field.placeholder,
217
required: field.required,
218
value: formData[field.name] || field.defaultValue || '',
219
onChange: (e: any) =>
220
setFormData(prev => ({
221
...prev,
222
[field.name]: e.target.type === 'checkbox' ? e.target.checked : e.target.value,
223
})),
224
};
225
226
return (
227
<div key={index} className="form-field">
228
<label>
229
{field.label || 'Loading...'}
230
{field.required && <span className="required">*</span>}
231
</label>
232
{field.type === 'textarea' ? (
233
<textarea {...commonProps} rows={4} />
234
) : field.type === 'select' ? (
235
<select {...commonProps}>
236
<option value="">Select...</option>
237
{field.options?.map((opt, i) => (
238
<option key={i} value={opt}>
239
{opt}
240
</option>
241
))}
242
</select>
243
) : field.type === 'checkbox' ? (
244
<input type="checkbox" {...commonProps} checked={formData[field.name] || false} />
245
) : (
246
<input type={field.type} {...commonProps} />
247
)}
248
</div>
249
);
250
};
251
252
return (
253
<div>
254
<div className="controls">
255
<button onClick={() => submit({ formType: 'contact' })} disabled={isLoading}>
256
Contact Form
257
</button>
258
<button onClick={() => submit({ formType: 'registration' })} disabled={isLoading}>
259
Registration Form
260
</button>
261
<button onClick={() => submit({ formType: 'survey' })} disabled={isLoading}>
262
Survey Form
263
</button>
264
{isLoading && <button onClick={stop}>Stop</button>}
265
<button onClick={clear}>Clear</button>
266
</div>
267
268
{isLoading && <div className="loading">Generating form...</div>}
269
270
{object && (
271
<form onSubmit={handleFormSubmit} className="generated-form">
272
<h2>{object.title || 'Loading form title...'}</h2>
273
274
{object.fields && object.fields.length > 0 ? (
275
<>
276
{object.fields.map((field, i) => renderField(field, i))}
277
{isComplete && (
278
<button type="submit">{object.submitButton || 'Submit'}</button>
279
)}
280
</>
281
) : (
282
<p>Loading fields...</p>
283
)}
284
</form>
285
)}
286
</div>
287
);
288
}
289
```
290
291
### Schema Evolution & Migration
292
293
```typescript
294
import { experimental_useObject } from '@ai-sdk/react';
295
import { z } from 'zod';
296
297
// Version 1 schema
298
const schemaV1 = z.object({
299
name: z.string(),
300
age: z.number(),
301
});
302
303
// Version 2 schema (added fields)
304
const schemaV2 = z.object({
305
name: z.string(),
306
age: z.number(),
307
email: z.string().email(),
308
address: z.object({
309
street: z.string(),
310
city: z.string(),
311
}),
312
});
313
314
function EvolvingSchemaExample() {
315
const [schemaVersion, setSchemaVersion] = useState<1 | 2>(1);
316
const currentSchema = schemaVersion === 1 ? schemaV1 : schemaV2;
317
318
const { object, submit, error } = experimental_useObject({
319
api: '/api/generate-data',
320
schema: currentSchema,
321
onFinish: ({ object, error }) => {
322
if (error) {
323
console.error('Schema validation failed, trying older schema');
324
// Could fallback to older schema version
325
if (schemaVersion === 2) {
326
setSchemaVersion(1);
327
}
328
} else {
329
console.log('Generated with schema v' + schemaVersion, object);
330
}
331
},
332
});
333
334
// Migrate old data to new schema
335
const migrateToV2 = (v1Data: z.infer<typeof schemaV1>): z.infer<typeof schemaV2> => {
336
return {
337
...v1Data,
338
email: '',
339
address: { street: '', city: '' },
340
};
341
};
342
343
return (
344
<div>
345
<select value={schemaVersion} onChange={(e) => setSchemaVersion(Number(e.target.value) as 1 | 2)}>
346
<option value={1}>Schema V1 (Basic)</option>
347
<option value={2}>Schema V2 (Extended)</option>
348
</select>
349
350
<button onClick={() => submit({ version: schemaVersion })}>
351
Generate (v{schemaVersion})
352
</button>
353
354
{error && <div>Error: {error.message}</div>}
355
{object && <pre>{JSON.stringify(object, null, 2)}</pre>}
356
</div>
357
);
358
}
359
```
360
361
### Real-time Form Validation
362
363
```typescript
364
import { experimental_useObject } from '@ai-sdk/react';
365
import { z } from 'zod';
366
import { useEffect } from 'react';
367
368
const profileSchema = z.object({
369
username: z.string().min(3).max(20),
370
bio: z.string().max(500),
371
website: z.string().url().optional(),
372
social: z.object({
373
twitter: z.string().optional(),
374
github: z.string().optional(),
375
}),
376
});
377
378
type Profile = z.infer<typeof profileSchema>;
379
380
function ProfileValidator() {
381
const [inputData, setInputData] = useState<Partial<Profile>>({});
382
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
383
384
const { object, submit, error } = experimental_useObject({
385
api: '/api/validate-profile',
386
schema: profileSchema,
387
});
388
389
// Validate on input change
390
useEffect(() => {
391
const validateField = async () => {
392
try {
393
profileSchema.parse(inputData);
394
setFieldErrors({});
395
} catch (err) {
396
if (err instanceof z.ZodError) {
397
const errors: Record<string, string> = {};
398
err.errors.forEach((e) => {
399
const path = e.path.join('.');
400
errors[path] = e.message;
401
});
402
setFieldErrors(errors);
403
}
404
}
405
};
406
407
if (Object.keys(inputData).length > 0) {
408
validateField();
409
}
410
}, [inputData]);
411
412
const handleChange = (field: string, value: string) => {
413
setInputData(prev => {
414
const newData = { ...prev };
415
const keys = field.split('.');
416
let current: any = newData;
417
418
for (let i = 0; i < keys.length - 1; i++) {
419
if (!current[keys[i]]) current[keys[i]] = {};
420
current = current[keys[i]];
421
}
422
423
current[keys[keys.length - 1]] = value;
424
return newData;
425
});
426
};
427
428
const handleSubmit = () => {
429
submit(inputData);
430
};
431
432
return (
433
<div>
434
<div className="form">
435
<div>
436
<input
437
placeholder="Username"
438
value={inputData.username || ''}
439
onChange={(e) => handleChange('username', e.target.value)}
440
/>
441
{fieldErrors.username && <span className="error">{fieldErrors.username}</span>}
442
</div>
443
444
<div>
445
<textarea
446
placeholder="Bio"
447
value={inputData.bio || ''}
448
onChange={(e) => handleChange('bio', e.target.value)}
449
/>
450
{fieldErrors.bio && <span className="error">{fieldErrors.bio}</span>}
451
</div>
452
453
<div>
454
<input
455
placeholder="Website"
456
value={inputData.website || ''}
457
onChange={(e) => handleChange('website', e.target.value)}
458
/>
459
{fieldErrors.website && <span className="error">{fieldErrors.website}</span>}
460
</div>
461
462
<button onClick={handleSubmit} disabled={Object.keys(fieldErrors).length > 0}>
463
Submit
464
</button>
465
</div>
466
467
{error && <div>Validation Error: {error.message}</div>}
468
{object && <div>Valid Profile: {JSON.stringify(object, null, 2)}</div>}
469
</div>
470
);
471
}
472
```
473
474
### Partial Object Updates
475
476
```typescript
477
import { experimental_useObject } from '@ai-sdk/react';
478
import { z } from 'zod';
479
480
const recipeSchema = z.object({
481
name: z.string(),
482
ingredients: z.array(z.object({
483
item: z.string(),
484
amount: z.string(),
485
})),
486
instructions: z.array(z.string()),
487
prepTime: z.number(),
488
cookTime: z.number(),
489
});
490
491
type Recipe = z.infer<typeof recipeSchema>;
492
493
function RecipeBuilder() {
494
const { object, submit, isLoading, clear } = experimental_useObject({
495
api: '/api/generate-recipe',
496
schema: recipeSchema,
497
experimental_throttle: 50,
498
});
499
500
// Track completion percentage
501
const calculateProgress = (obj: DeepPartial<Recipe> | undefined): number => {
502
if (!obj) return 0;
503
let completed = 0;
504
let total = 5; // name, ingredients, instructions, prepTime, cookTime
505
506
if (obj.name) completed++;
507
if (obj.ingredients && obj.ingredients.length > 0) completed++;
508
if (obj.instructions && obj.instructions.length > 0) completed++;
509
if (obj.prepTime !== undefined) completed++;
510
if (obj.cookTime !== undefined) completed++;
511
512
return Math.round((completed / total) * 100);
513
};
514
515
const progress = calculateProgress(object);
516
517
return (
518
<div>
519
<button onClick={() => submit({ dish: 'pasta carbonara' })} disabled={isLoading}>
520
Generate Recipe
521
</button>
522
523
{isLoading && (
524
<div className="progress-bar">
525
<div className="progress-fill" style={{ width: `${progress}%` }}>
526
{progress}%
527
</div>
528
</div>
529
)}
530
531
{object && (
532
<div className="recipe">
533
<h1>{object.name || '⏳ Generating name...'}</h1>
534
535
<div className="meta">
536
<span>Prep: {object.prepTime !== undefined ? `${object.prepTime} min` : '⏳'}</span>
537
<span>Cook: {object.cookTime !== undefined ? `${object.cookTime} min` : '⏳'}</span>
538
</div>
539
540
<div className="ingredients">
541
<h2>Ingredients {object.ingredients ? `(${object.ingredients.length})` : ''}</h2>
542
{object.ingredients && object.ingredients.length > 0 ? (
543
<ul>
544
{object.ingredients.map((ing, i) => (
545
<li key={i}>{ing.amount} {ing.item}</li>
546
))}
547
</ul>
548
) : (
549
<p>⏳ Loading ingredients...</p>
550
)}
551
</div>
552
553
<div className="instructions">
554
<h2>Instructions {object.instructions ? `(${object.instructions.length} steps)` : ''}</h2>
555
{object.instructions && object.instructions.length > 0 ? (
556
<ol>
557
{object.instructions.map((step, i) => (
558
<li key={i}>{step}</li>
559
))}
560
</ol>
561
) : (
562
<p>⏳ Loading instructions...</p>
563
)}
564
</div>
565
566
{!isLoading && progress === 100 && (
567
<div className="complete">✓ Recipe Complete!</div>
568
)}
569
</div>
570
)}
571
572
<button onClick={clear}>Clear</button>
573
</div>
574
);
575
}
576
```
577
578
### Shared State Pattern
579
580
```typescript
581
// components/ObjectDisplay.tsx
582
import { experimental_useObject } from '@ai-sdk/react';
583
import { z } from 'zod';
584
585
const schema = z.object({
586
title: z.string(),
587
content: z.string(),
588
});
589
590
export function ObjectDisplay() {
591
const { object, isLoading } = experimental_useObject({
592
id: 'shared-object',
593
api: '/api/generate',
594
schema,
595
});
596
597
return (
598
<div className="display">
599
{isLoading && <div>Loading...</div>}
600
{object && (
601
<div>
602
<h2>{object.title}</h2>
603
<p>{object.content}</p>
604
</div>
605
)}
606
</div>
607
);
608
}
609
610
// components/ObjectControls.tsx
611
export function ObjectControls() {
612
const { submit, stop, clear, isLoading } = experimental_useObject({
613
id: 'shared-object',
614
api: '/api/generate',
615
schema,
616
});
617
618
return (
619
<div className="controls">
620
<button onClick={() => submit({ prompt: 'Generate' })} disabled={isLoading}>
621
Generate
622
</button>
623
{isLoading && <button onClick={stop}>Stop</button>}
624
<button onClick={clear}>Clear</button>
625
</div>
626
);
627
}
628
```
629
630
## Schema Types
631
632
```typescript { .api }
633
// Base schema interface
634
interface Schema<T = unknown> {
635
validate(value: unknown): { success: true; value: T } | { success: false; error: Error };
636
}
637
638
// Zod schema type
639
type ZodType = import('zod').ZodType;
640
641
// Infer TypeScript type from schema
642
type InferSchema<SCHEMA> =
643
SCHEMA extends Schema<infer T> ? T :
644
SCHEMA extends ZodType ? import('zod').infer<SCHEMA> :
645
unknown;
646
647
// Deeply partial type for streaming
648
type DeepPartial<T> = T extends object
649
? { [P in keyof T]?: DeepPartial<T[P]> }
650
: T;
651
```
652
653
### Custom Schema Example
654
655
```typescript
656
import { experimental_useObject } from '@ai-sdk/react';
657
658
// Custom schema implementation
659
const customSchema = {
660
validate(value: unknown) {
661
if (typeof value === 'object' && value !== null) {
662
const obj = value as any;
663
if (
664
typeof obj.name === 'string' &&
665
typeof obj.age === 'number' &&
666
obj.age >= 0 &&
667
obj.age <= 150
668
) {
669
return { success: true, value: obj as { name: string; age: number } };
670
}
671
}
672
return {
673
success: false,
674
error: new Error('Invalid data: expected { name: string, age: number }'),
675
};
676
},
677
};
678
679
function CustomSchemaExample() {
680
const { object, submit } = experimental_useObject({
681
api: '/api/generate',
682
schema: customSchema,
683
});
684
685
return (
686
<div>
687
<button onClick={() => submit({ prompt: 'Generate person' })}>Generate</button>
688
{object && (
689
<div>
690
<p>Name: {object.name}</p>
691
<p>Age: {object.age}</p>
692
</div>
693
)}
694
</div>
695
);
696
}
697
```
698
699
## Best Practices
700
701
1. **Use strict schemas**: Define precise validation rules to catch errors early
702
2. **Handle validation errors**: Implement `onFinish` to handle schema validation failures
703
3. **Progressive rendering**: Check for undefined fields and show loading states
704
4. **Type safety**: Use `z.infer<typeof schema>` for proper TypeScript types
705
5. **Error recovery**: Provide retry mechanisms for validation failures
706
6. **Show progress**: Display completion percentage for better UX
707
7. **Validate input**: Validate user input before submitting
708
8. **Schema versioning**: Plan for schema evolution and migration
709
9. **Optimize throttling**: Use `experimental_throttle` for large objects
710
10. **Clear state**: Use `clear()` to reset between generations
711
712
## Common Zod Patterns
713
714
```typescript
715
// Optional fields
716
z.object({
717
required: z.string(),
718
optional: z.string().optional(),
719
});
720
721
// Default values
722
z.object({
723
name: z.string().default('Unknown'),
724
});
725
726
// Enums
727
z.object({
728
status: z.enum(['draft', 'published', 'archived']),
729
});
730
731
// Arrays with validation
732
z.object({
733
tags: z.array(z.string()).min(1).max(10),
734
});
735
736
// Nested objects
737
z.object({
738
user: z.object({
739
name: z.string(),
740
email: z.string().email(),
741
}),
742
});
743
744
// Unions
745
z.object({
746
value: z.union([z.string(), z.number()]),
747
});
748
749
// Refinements (custom validation)
750
z.object({
751
password: z.string().min(8).refine(
752
(val) => /[A-Z]/.test(val),
753
'Must contain uppercase letter'
754
),
755
});
756
```
757