JSON Schemas and Validation
On this page
JSON Schemas and Validation
A function schema isn’t validation plumbing: it’s the only contract between the model and your code. There is no other channel. The model doesn’t read your implementation, doesn’t know your internal conventions, and won’t guess that a date field expects ISO format instead of French format.
Anything not written in the schema will be decided by the model’s random inferences. This explains the effort imbalance recommended in this lesson: it’s better to spend ten minutes on a schema than two hours debugging erratic arguments.
Function Schema Structure
Every function declared in tools follows the JSON Schema format. Here’s the complete structure:
tool_definition = {
"type": "function",
"name": "creer_facture",
"description": "Create an invoice for an existing customer",
"parameters": {
"type": "object",
"properties": {
"client_id": {
"type": "string",
"description": "Unique customer identifier (format: CLI-XXXXX)"
},
"lignes": {
"type": "array",
"description": "List of billing line items",
"items": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "Product or service description"
},
"quantite": {
"type": "integer",
"minimum": 1,
"description": "Number of units"
},
"prix_unitaire": {
"type": "number",
"minimum": 0,
"description": "Unit price excluding tax in euros"
}
},
"required": ["description", "quantite", "prix_unitaire"]
}
},
"devise": {
"type": "string",
"enum": ["EUR", "USD", "GBP"],
"default": "EUR"
},
"echeance_jours": {
"type": "integer",
"enum": [30, 45, 60, 90],
"description": "Payment deadline in days"
}
},
"required": ["client_id", "lignes"],
"additionalProperties": False
},
"strict": True
}
Strict Mode
Strict mode turns probability into a guarantee. Without it, the model usually produces compliant arguments—and you discover the exceptions in production, on edge cases. With it, compliance is ensured by construction at the decoding level. Enable it by default: the constraints it imposes, detailed just below, are a small price to pay for eliminating an entire class of bugs.
tools = [{
"type": "function",
"name": "enregistrer_commande",
"description": "Record a new order",
"parameters": {
"type": "object",
"properties": {
"produit": {"type": "string"},
"quantite": {"type": "integer"},
"urgent": {"type": "boolean"}
},
"required": ["produit", "quantite", "urgent"],
"additionalProperties": False
},
"strict": True
}]
Strict Mode Constraints
In strict mode, certain rules apply:
- All fields must be in
required additionalPropertiesmust befalse- Optional types use
anyOfwithnull - No
pattern,minLength,maxLengthat the top level
# Optional field in strict mode
"commentaire": {
"anyOf": [
{"type": "string"},
{"type": "null"}
],
"description": "Optional free-text comment"
}
Client-Side Validation
Strict mode guarantees form, never meaning. A well-formed date might be in the past, a well-typed identifier might not match any customer, and a valid amount might exceed the authorized limit. These rules are business logic: no JSON schema expresses them, and they must be verified in your code—with, in case of rejection, an error message sent back to the model explaining what went wrong.
import json
from jsonschema import validate, ValidationError
SCHEMA_FACTURE = {
"type": "object",
"properties": {
"client_id": {"type": "string", "pattern": "^CLI-[0-9]{5}$"},
"lignes": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"description": {"type": "string", "minLength": 1},
"quantite": {"type": "integer", "minimum": 1},
"prix_unitaire": {"type": "number", "minimum": 0}
},
"required": ["description", "quantite", "prix_unitaire"]
}
}
},
"required": ["client_id", "lignes"]
}
def executer_fonction(name: str, arguments: str) -> str:
args = json.loads(arguments)
try:
validate(instance=args, schema=SCHEMA_FACTURE)
except ValidationError as e:
return json.dumps({"error": f"Invalid arguments: {e.message}"})
# Business logic after validation
return creer_facture(**args)
Effective Descriptions
Write a function description like instructions for a new colleague: when to call it, and especially when not to call it. It’s that second part we systematically forget, yet it solves most erroneous calls—“do not use for archived orders” prevents an unnecessary round trip that three sentences of positive description wouldn’t have stopped.
# Bad: too vague
{
"name": "get_data",
"description": "Retrieve data"
}
# Good: specific and contextual
{
"name": "rechercher_produits_catalogue",
"description": "Search for products in the e-commerce catalog. "
"Returns name, price, stock, and category. "
"Use for questions about availability or pricing."
}
Parameter Descriptions
At the parameter level, the enemy is the implicit. A unit (euros or cents?), a format (ISO or local?), a convention (internal ID or customer reference?) that aren’t written will be guessed—sometimes correctly, sometimes not, with no signal either way. The enum is your best tool here: it doesn’t describe the expected value, it constrains it.
"properties": {
"date_debut": {
"type": "string",
"description": "Start date in ISO 8601 format (YYYY-MM-DD). "
"Defaults to today's date if not specified."
},
"statut": {
"type": "string",
"enum": ["actif", "inactif", "suspendu"],
"description": "Filter by account status. "
"'actif' = accounts in service, "
"'suspendu' = temporarily frozen accounts."
}
}
Nested Schemas and References
A deeply nested schema is hard for you to reread and for the model to fill out correctly. If you reach three levels, the question to ask isn’t how to write it better but whether the function is doing too much: two functions with clear scope are almost always preferable to one configurable function that covers both cases.
adresse_schema = {
"type": "object",
"properties": {
"rue": {"type": "string"},
"code_postal": {"type": "string"},
"ville": {"type": "string"},
"pays": {"type": "string", "enum": ["FR", "BE", "CH", "LU"]}
},
"required": ["rue", "code_postal", "ville", "pays"]
}
tools = [{
"type": "function",
"name": "creer_livraison",
"description": "Create a delivery with shipping and destination addresses",
"parameters": {
"type": "object",
"properties": {
"expedition": adresse_schema,
"destination": adresse_schema,
"poids_kg": {"type": "number", "minimum": 0.1}
},
"required": ["expedition", "destination", "poids_kg"],
"additionalProperties": False
},
"strict": True
}]
Key Takeaways
- Enable
strict: trueto guarantee argument compliance - Still validate client-side with
jsonschemafor business rules - Write precise descriptions: they’re the primary quality lever
- Use
enumto constrain accepted values - Break complex schemas into reusable sub-structures