JSON response
One prompt. The reply is a JSON object with a string, a boolean, and a number.
This example sends one prompt and returns one JSON object with a string, a boolean, and a number. The model returns a complete structured response with no intermediate text events.
- Live demo: react.fency.ai/json-response
- GitHub folder: fency-react-examples/app/json-response
Prerequisites
You need a publishable key for the React SDK and a secret key for your server routes. This example uses sessions to authenticate the client.
The example uses Clerk for user authentication. That is part of the app shell and not specific to Fency.
Server session routes
The example defines two session routes: one for stream sessions and one for
agent task sessions. Each route is standalone so you can read a single file
and see the full POST /v1/sessions call.
Create the stream session route at api/create-stream-session/route.ts:
import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { sessionClientTokenSchema } from '../../sessionClientTokenSchema'
export async function POST() {
const userId = await getAuthorizedUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const secretKey = process.env.FENCY_SECRET_KEY
if (!secretKey) {
throw new Error('FENCY_SECRET_KEY is not defined.')
}
const response = await fetch('https://api.fency.ai/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ createStream: {} }),
})
if (!response.ok) {
throw new Error('Failed to create Fency session.')
}
return NextResponse.json(sessionClientTokenSchema.parse(await response.json()))
}Create the agent task session route at api/create-agent-task-session/route.ts:
import { NextResponse } from 'next/server'
import { getAuthorizedUserId } from '../../../auth'
import { sessionClientTokenSchema } from '../../sessionClientTokenSchema'
export async function POST() {
const userId = await getAuthorizedUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const secretKey = process.env.FENCY_SECRET_KEY
if (!secretKey) {
throw new Error('FENCY_SECRET_KEY is not defined.')
}
const response = await fetch('https://api.fency.ai/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
createAgentTask: {
taskType: 'STRUCTURED_CHAT_COMPLETION',
metadata: { userId },
},
}),
})
if (!response.ok) {
throw new Error('Failed to create Fency session.')
}
return NextResponse.json(sessionClientTokenSchema.parse(await response.json()))
}Response schema
Define the response schema using Zod in responseSchema.ts. It includes a
string, a boolean, and a number:
import { z } from 'zod'
export const responseSchema = z.object({
name: z.string().describe('City name'),
coastal: z.boolean().describe('Whether the city sits on a coast'),
population: z.number().describe('Approximate population'),
})
export type JsonResponse = z.infer<typeof responseSchema>
export const responseJsonSchema = JSON.stringify(z.toJSONSchema(responseSchema))The schema descriptions guide the model on what to return for each field.
Client provider and hooks
Both session routes respond with a client token. Parse that response with a
Zod schema in sessionClientTokenSchema.ts so a bad shape throws:
import { z } from 'zod'
export const sessionClientTokenSchema = z.object({
clientToken: z.string(),
})Load the Fency client and wrap your component tree with FencyProvider in page.tsx:
'use client'
import { loadFency } from '@fencyai/js'
import { FencyProvider } from '@fencyai/react'
import { JsonViewer } from './components/JsonViewer'
import { sessionClientTokenSchema } from './sessionClientTokenSchema'
const publishableKey = process.env.NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY
if (!publishableKey) {
throw new Error('NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY is not defined.')
}
const fency = loadFency({
publishableKey,
})
async function fetchCreateStreamClientToken() {
const res = await fetch('/json-response/api/create-stream-session', {
method: 'POST',
})
if (!res.ok) {
throw new Error('Failed to create stream session')
}
const { clientToken } = sessionClientTokenSchema.parse(await res.json())
return { clientToken }
}
export default function JsonResponsePage() {
return (
<FencyProvider
fency={fency}
fetchCreateStreamClientToken={fetchCreateStreamClientToken}
>
<JsonViewer />
</FencyProvider>
)
}Use useAgentTasks and createAgentTask to get the JSON object in hooks/useJsonResponse.ts:
'use client'
import { useAgentTasks } from '@fencyai/react'
import { useState } from 'react'
import {
responseJsonSchema,
responseSchema,
type JsonResponse,
} from '../responseSchema'
import { sessionClientTokenSchema } from '../sessionClientTokenSchema'
async function fetchCreateAgentTaskClientToken() {
const res = await fetch('/json-response/api/create-agent-task-session', {
method: 'POST',
})
if (!res.ok) {
throw new Error('Failed to create agent task session')
}
const { clientToken } = sessionClientTokenSchema.parse(await res.json())
return { clientToken }
}
export function useJsonResponse() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [latestResult, setLatestResult] = useState<JsonResponse | null>(null)
const { latest, createAgentTask } = useAgentTasks({})
async function getJson(prompt: string) {
setIsSubmitting(true)
setLatestResult(null)
try {
const response = await createAgentTask(
{
type: 'StructuredChatCompletion',
messages: [{ role: 'USER', content: prompt }],
model: 'anthropic/claude-sonnet-4.6',
jsonSchema: responseJsonSchema,
},
{ fetchCreateAgentTaskClientToken },
)
if (response.type !== 'success') {
throw new Error(response.error.message)
}
if (response.response.taskType !== 'StructuredChatCompletion') {
throw new Error('Unexpected StructuredChatCompletion outcome.')
}
setLatestResult(
responseSchema.parse(
JSON.parse(response.response.response.jsonResponse),
),
)
} finally {
setIsSubmitting(false)
}
}
return {
latestTask: latest,
latestResult,
isSubmitting,
getJson,
}
}Render progress and the parsed object in components/JsonViewer.tsx:
'use client'
import { Alert, Badge, Text, Title } from '@mantine/core'
import { AgentTaskProgress } from '@fencyai/react'
import { useJsonResponse } from '../hooks/useJsonResponse'
import { JsonResult } from './JsonResult'
import { PromptForm } from './PromptForm'
export function JsonViewer() {
const { latestTask, latestResult, isSubmitting, getJson } = useJsonResponse()
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-6">
<div>
<Badge size="sm" variant="light" color="green" mb={4}>
Basic
</Badge>
<Title order={1} size="h4">
JSON response
</Title>
<Text size="sm" c="dimmed">
Name a city. The reply is a JSON object with a string, a boolean, and
a number.
</Text>
</div>
<PromptForm isSubmitting={isSubmitting} onGetJson={getJson} />
{latestTask?.error ? (
<Alert color="red">{latestTask.error.message}</Alert>
) : latestTask ? (
<AgentTaskProgress agentTask={latestTask} />
) : null}
{latestResult ? <JsonResult result={latestResult} /> : null}
</div>
)
}The AgentTaskProgress component displays progress while the task runs. The result is parsed with the Zod schema to ensure type safety. components/PromptForm.tsx owns the textarea and calls getJson. components/JsonResult.tsx shows the parsed object as formatted JSON.
Running locally
Clone the repository and install dependencies:
git clone https://github.com/fencyai/fency-react-examples.git
cd fency-react-examples
npm installCopy .env.example to .env.local and add your Fency keys:
cp .env.example .env.localSet the keys in .env.local:
FENCY_SECRET_KEY=sk_...
NEXT_PUBLIC_FENCY_PUBLISHABLE_KEY=pk_...Initialize Clerk authentication (provisions a development app):
npx -y clerk@latest init --keyless -yStart the development server:
npm run devOpen http://localhost:3000/json-response. Sign up from the header, then send a prompt to get a JSON object with a string, a boolean, and a number.