or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mduse-chat.mduse-completion.mduse-object.md

use-object.mddocs/

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