خروجی ساختیافته (JSON)
گرفتن JSON معتبر از مدل با response_format از نوع json_object و json_schema، نمونهٔ Python و Node، و اشتباههای رایج با متن و اعداد فارسی.
بهروزرسانی: ۱۶ شهریور ۱۴۰۵
وقتی خروجی مدل قرار است وارد کد شود (پر کردن فرم، استخراج فیلد از فاکتور، دستهبندی)، متن آزاد کافی نیست. response_format به مدل میگوید فقط JSON برگرداند و در حالت json_schema شکل دقیق آن را هم تحمیل میکند. یوتاپن این فیلد را عیناً عبور میدهد.
کدام مدلها
نشان json در صفحهٔ مدلها یعنی مدل response_format را میفهمد؛ در GET /v1/models معادلش "response_format" و برای اسکیمای سختگیرانه "structured_outputs" در supported_parameters است. مدلی که structured_outputs ندارد ممکن است json_schema را با 400 رد کند یا فقط بهصورت «حالت JSON» رفتار کند.
حالت json_object
سادهترین شکل: مدل تضمین میکند خروجی یک JSON معتبر است، ولی اسکیما را تو در prompt توضیح میدهی.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.uttapen.ir/v1", api_key="sk-up-...")
resp = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=[
{"role": "system", "content": "فقط JSON برگردان با کلیدهای name، city و phone. مقدار نامعلوم null."},
{"role": "user", "content": "علی رضایی از اصفهان، شماره ۰۹۱۳۱۲۳۴۵۶۷"},
],
response_format={"type": "json_object"},
)
data = json.loads(resp.choices[0].message.content)
print(data["city"])
قانون providerها: کلمهٔ «JSON» باید جایی در پیامها آمده باشد وگرنه درخواست رد میشود. همان system prompt بالا کافی است.
حالت json_schema
اسکیمای JSON Schema میدهی و مدل خروجی را دقیقاً با آن تطبیق میدهد. با strict: true هیچ فیلد اضافه یا کم نمیآید.
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"city": {"type": ["string", "null"]},
"phone": {"type": ["string", "null"], "description": "با ارقام لاتین و بدون فاصله"},
"intent": {"type": "string", "enum": ["order", "complaint", "question"]},
},
"required": ["name", "city", "phone", "intent"],
"additionalProperties": False,
}
resp = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=[{"role": "user", "content": "علی رضایی از اصفهان زنگ زد و از تأخیر سفارش شاکی بود. ۰۹۱۳۱۲۳۴۵۶۷"}],
response_format={
"type": "json_schema",
"json_schema": {"name": "contact", "strict": True, "schema": schema},
},
)
contact = json.loads(resp.choices[0].message.content)
در حالت strict همهٔ فیلدها باید در required باشند و additionalProperties: false الزامی است؛ برای فیلد اختیاری از نوع ["string", "null"] استفاده کن. اگر SDK Python را با Pydantic به کار میبری، client.chat.completions.parse(..., response_format=ContactModel) همین اسکیما را از کلاس میسازد و خروجی را در resp.choices[0].message.parsed میدهد.
Node.js با Zod
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const Contact = z.object({
name: z.string(),
city: z.string().nullable(),
intent: z.enum(["order", "complaint", "question"]),
});
const client = new OpenAI({ baseURL: "https://api.uttapen.ir/v1", apiKey: process.env.UTTAPEN_API_KEY });
const resp = await client.chat.completions.parse({
model: "openai/gpt-5-mini",
messages: [{ role: "user", content: "علی رضایی از اصفهان، سؤال دربارهٔ گارانتی" }],
response_format: zodResponseFormat(Contact, "contact"),
});
console.log(resp.choices[0].message.parsed);
اشتباههای رایج با متن فارسی
- ارقام فارسی. مدلها گاهی
"phone": "۰۹۱۳..."میدهند که برایint()یا regex تو بیمعنی است. درdescriptionفیلد بنویس «ارقام لاتین» و در کد هم یک نرمالساز ساده بگذار: جایگزینی۰-۹و٠-٩با0-9. - عدد بهصورت رشته. مبلغ «۱٬۲۵۰٬۰۰۰ تومان» ممکن است رشته برگردد. نوع را در اسکیما
numberبگذار و در prompt بگو بدون جداکنندهٔ هزارگان. - Unicode escape. بعضی مدلها فارسی را به شکل
علیمینویسند. این JSON کاملاً معتبر است وjson.loadsدرستش میکند؛ فقط در لاگ خام زشت است. لازم نیست کاری بکنی. - کاراکترهای نامرئی. نیمفاصله (
) و نشانههای جهت () در مقدار رشتهها میآیند و مقایسهٔ دقیق را خراب میکنند. قبل از مقایسه با مقادیر ثابت (مثل نام شهر)، ازenumاستفاده کن یا نرمالسازی کن. - ی و ک عربی. «ي» (U+064A) و «ك» (U+0643) در ورودی کاربر رایجاند و در خروجی مدل برمیگردند. اگر مقادیر را در دیتابیس جستجو میکنی، هر دو طرف را به «ی» و «ک» فارسی تبدیل کن.
- متن قبل یا بعد از JSON. در حالت
json_objectبعضی مدلها بلوک```jsonمیگذارند.json_schemaاین مشکل را ندارد. اگر مجبوری از مدلی بدون آن استفاده کنی، اولین{تا آخرین}را ببُر.
وقتی مدل json_schema ندارد
با tool calling و tool_choice اجباری، آرگومانهای تابع در عمل JSON مطابق اسکیمای تو هستند. برای مدلهای متنباز که structured_outputs ندارند ولی tools دارند، این راه قابلاعتمادتر از prompt است.
هزینه
اسکیما جزو prompt نیست ولی providerها معمولاً آن را بهصورت grammar روی decoder اعمال میکنند و بعضی برای بار اول کمی کندترند. خروجی JSON فشردهتر از متن آزاد است و توکن کمتری مصرف میکند. max_tokens را متناسب با بزرگترین خروجی ممکن بگذار؛ JSON بریدهشده (finish_reason: "length") قابل parse نیست و پولش را دادهای.