Structured Output

Gerbil can make a model return a validated JSON object instead of free-form text. It generates, extracts the JSON from the output, validates it against your schema, and — if validation fails — retries with a corrective nudge until the result is valid or the retry budget runs out. Because on-device tokens are free, re-rolling malformed JSON costs nothing but a moment, so you get a typed object back rather than a string you have to defensively parse.

useObject in the browser

The useObject hook is the React entry point. Type it with the shape you expect, call generate with a prompt and a schema, and read the parsed result from object:

Extractor.tsx
01"use client";
02
03import { useObject } from "@tryhamster/gerbil/hooks";
04
05type Person = { name: string; age: number };
06
07function Extractor() {
08 const { object, generate, attempts, isGenerating } = useObject<Person>();
09
10 return (
11 <div>
12 <button
13 onClick={() =>
14 generate('Extract {name, age} from: "I am Sarah, 28"', {
15 schema: { required: ["name", "age"] },
16 })
17 }
18 disabled={isGenerating}
19 >
20 {isGenerating ? "Extracting…" : "Extract"}
21 </button>
22 {object && (
23 <p>
24 {object.name} is {object.age} (took {attempts} attempt
25 {attempts === 1 ? "" : "s"})
26 </p>
27 )}
28 </div>
29 );
30}

The hook returns the parsed object (null until one is produced), attempts (1 means it parsed on the first try), plus the usual isGenerating, isLoading, isReady, stop, and error lifecycle fields.

How validation works

The schema option is an ObjectValidator. It can take one of three forms:

  • A minimal schema object { required: ["name", "age"] }. Every key in required must exist on the parsed object.
  • A predicate (o) => boolean. Full control: return true when the parsed value is acceptable.
  • Omitted — any syntactically valid JSON value (object or array) is accepted.

Use a predicate when key presence isn't enough and you need to check types or values:

Primes.tsx
01"use client";
02
03import { useObject } from "@tryhamster/gerbil/hooks";
04
05type Primes = { primes: number[] };
06
07function Primes() {
08 const { object, generate, isGenerating } = useObject<Primes>();
09
10 return (
11 <div>
12 <button
13 disabled={isGenerating}
14 onClick={() =>
15 generate("List the first 3 primes as {primes: number[]}", {
16 // Predicate: enforce the field is an array of numbers.
17 schema: (o) =>
18 Array.isArray(o.primes) &&
19 o.primes.every((n) => typeof n === "number"),
20 maxRetries: 6,
21 })
22 }
23 >
24 Generate
25 </button>
26 {object && <pre>{JSON.stringify(object)}</pre>}
27 </div>
28 );
29}

maxRetries (default 4) is the number of retries after the first attempt, so up to maxRetries + 1 generations run before it gives up.

Imperatively with generateObject

When you need structured output outside React — or alongside other capabilities on a shared engine — call generateObject on the engine. It runs the same generate → parse → validate → retry loop and returns the parsed object plus the attempt count:

generate-object.ts
01import { useEngine } from "@tryhamster/gerbil/hooks";
02
03type Person = { name: string; age: number };
04
05function Extractor() {
06 const { generateObject, isReady } = useEngine();
07
08 const extract = async () => {
09 const { object, attempts } = await generateObject<Person>(
10 'Extract {name, age} from: "I am Sarah, 28"',
11 { schema: { required: ["name", "age"] }, maxRetries: 4 }
12 );
13 console.log(object, "in", attempts, "attempt(s)");
14 // { name: "Sarah", age: 28 }
15 };
16
17 return (
18 <button onClick={extract} disabled={!isReady}>
19 Extract
20 </button>
21 );
22}

generateObject extends the engine's generation options, so maxTokens, system, and temperature all apply. A low temperature (e.g. 0.2) tends to make extraction more reliable.

Types

The structured-output types are exported from @tryhamster/gerbil/hooks if you want to annotate validators or options yourself:

types.ts
01import type {
02 ObjectValidator,
03 GenerateObjectOptions,
04} from "@tryhamster/gerbil/hooks";
05
06// A reusable typed validator.
07const isPerson: ObjectValidator<{ name: string; age: number }> = (o) =>
08 typeof o.name === "string" && typeof o.age === "number";
09
10const options: GenerateObjectOptions = {
11 schema: isPerson,
12 maxRetries: 4,
13 temperature: 0.2,
14};

See also

  • React Hooks — the full hook set, including useObject in context.
  • Tools — function calling when you need the model to invoke code, not just emit data.