Getting strictly structured JSON out of the model
Asking the model to "reply in JSON" in the system prompt works about half the time. Here is a more reliable approach using tools with a strict schema.
3 min · · API · development
If the only defense against broken JSON is the phrase "respond strictly in JSON format" in the system prompt, sooner or later the model will wrap the answer in an explanation, add text before or after the braces, or drop a closing quote. There's a more reliable approach — instead of asking, constrain the structure directly.
Why a prompt request isn't a guarantee
{"role": "system", "content": "Respond only in JSON format, no explanations"}This kind of prompt works most of the time, but it's not a formal guarantee. The model is a text generator, and a system prompt instruction is a strong hint, not a protocol-level constraint. On long or ambiguous requests the model might add a preamble like "Here's the requested JSON:", wrap the object in a ```json code block, or produce invalid JSON with a trailing comma. Your parser breaks, and you end up writing regexes to clean the response before json.loads.
A more reliable approach: tools with a strict schema
Both /v1/responses and /v1/chat/completions accept a tools parameter — a set of functions with a description and a JSON schema for their parameters. The standard use of tools is letting the model call external functions, but the same mechanism works great as a way to force structured output: you describe a "function" whose parameters are exactly the data structure you need, and the model has to fill them in according to the schema rather than compose free-form text.
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_your_key" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"messages": [
{"role": "user", "content": "Extract name, age, and city from: My name is Igor, I am 34, I live in Kazan"}
],
"tools": [{
"type": "function",
"function": {
"name": "extract_person",
"description": "Extracts structured data about a person from text",
"parameters": {The key part is tool_choice, which forces the model to call exactly this function instead of answering with free text at its own discretion. The response arrives not as ordinary text, but as a function call with arguments in JSON, strictly matching the declared schema:
{
"choices": [{
"message": {
"tool_calls": [{
"function": {
"name": "extract_person",
"arguments": "{\"name\":\"Igor\",\"age\":34,\"city\":\"Kazan\"}"
}
}]
}
}]
}The arguments field is a string containing valid JSON, ready to parse directly with no cleanup of stray text.
The same thing in Python
import json
import requests
def extract_structured(text, api_key):
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"},
},
"required": ["name", "age", "city"],
}
resp = requests.post(The same trick over the Anthropic-compatible /v1/messages
Anthropic-style requests to /v1/messages support tools in a similar spirit: you describe a tool with a schema and force the model to call it. The declaration format differs slightly (input_schema instead of parameters nested under function), but the idea is the same — the structure is guaranteed by the schema, not requested in prose.
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_your_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Extract name, age, and city: My name is Igor, I am 34, I live in Kazan"}],
"tools": [{
"name": "extract_person",
"description": "Extracts structured data about a person",
"input_schema": {
"type": "object",
"properties": {Nested structures or lists of objects
The parameters / input_schema field is a regular JSON Schema, so nested objects and arrays are described the standard way:
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"price": {"type": "number"}
},
"required": ["title", "price"]
}
}
},
"required": ["items"]
}The model fills the items array with as many elements as the source text warrants, each strictly matching the nested schema.
Validation is still necessary
A schema sharply reduces the chance of garbage in the response, but it doesn't eliminate the need to check things on your end: an integer type in the schema doesn't guarantee the number lands in a sensible range, and a required field doesn't guarantee the model won't fill it with an empty string when the source text is missing that information. It's still sensible to run the result through validation (pydantic, zod, or manual checks) before feeding it into business logic.
Bottom line
A request in the prompt is a hint, not a contract. If you need a structure you can rely on in code without defensive parsing, use tools with an explicit JSON schema and a tool_choice that forces the specific function — available both in OpenAI-compatible mode (/v1/chat/completions, /v1/responses) and in Anthropic-compatible mode (/v1/messages).
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.