JSON Design Patterns for AI-Friendly APIs (With Code Examples)
Practical JSON schema patterns that make your API responses parseable by AI agents: stable field naming, typed responses, nested structures, enums, and validation strategies.
JSON Design Patterns for AI-Friendly APIs (With Code Examples)
Your API returns JSON. So does every other API built in the last decade. But there is a growing gap between APIs that return JSON and APIs that return JSON an AI agent can reliably parse, interpret, and act on.
The difference is not in the format. It is in the design patterns: how you name fields, structure nested objects, handle optional values, type your responses, and document what each field means. These patterns determine whether an AI agent can consume your API responses accurately or whether it hallucinates interpretations, drops data, or feeds wrong information to users.
This guide covers the specific JSON design patterns that make the difference, with code examples in TypeScript (Zod) and Python (Pydantic) since those are the two dominant schema definition tools in the API ecosystem.
Why JSON Design Matters for AI Agents
When a human developer integrates your API, they read the documentation, understand the context, and write parsing code that handles quirks. If a field is sometimes a string and sometimes a number, they add a type check. If field names change between endpoints, they write per-endpoint parsers.
AI agents do not have this luxury. They process your JSON schema (from OpenAPI or MCP) to understand the structure, then parse responses based on that schema. Three failure modes dominate:
Schema drift: Your API returns customer_name in one endpoint and customerName in another. The agent treats these as different fields and loses data continuity across a multi-step workflow.
Ambiguous types: A field returns "15.42" (string) in some responses and 15.42 (number) in others. The agent parsing logic breaks on the inconsistency, or worse, silently treats the string as a different data type.
Missing context: A response field called rx with value true means nothing to an agent. Is it a prescription? A reactive extension? Without a description, the agent cannot relay this information accurately to the user. (It means retrograde, in astrology.)
All three failure modes are preventable with consistent design patterns.
Pattern 1: Consistent Field Naming Convention
Pick one naming convention and enforce it across every endpoint, every response object, and every nested structure.
The Rule
Use snake_case for JSON field names. It is the most common convention in REST APIs, maps naturally to Python (the dominant AI/ML language), and avoids ambiguity with acronyms.
Consistent (agent learns one pattern):
{
"birth_date": "1990-06-15",
"birth_time": "14:30",
"house_system": "placidus",
"is_retrograde": true,
"ecliptic_longitude": 84.23
}
Inconsistent (agent must handle three naming styles):
{
"birthDate": "1990-06-15",
"birth_time": "14:30",
"houseSystem": "placidus",
"IsRetrograde": true,
"ecliptic-longitude": 84.23
}
Why It Matters for Agents
AI agents often construct natural language responses from field names. is_retrograde becomes "is retrograde" naturally. IsRetrograde or isRetrograde requires case-splitting logic. Consistency reduces the number of patterns the agent must handle.
Implementation
In Zod (TypeScript):
const planetSchema = z.object({
name: z.string(),
ecliptic_longitude: z.number(),
ecliptic_latitude: z.number(),
is_retrograde: z.boolean(),
speed_degrees_per_day: z.number(),
});
In Pydantic (Python):
class Planet(BaseModel):
name: str
ecliptic_longitude: float
ecliptic_latitude: float
is_retrograde: bool
speed_degrees_per_day: float
Pattern 2: Explicit Types, Never Ambiguous
Every field should have one type, always. Never return a field that is sometimes a string and sometimes a number, sometimes an object and sometimes null without documentation.
The Anti-Patterns
String-encoded numbers (agent must guess if it should parse):
{ "longitude": "84.23", "distance_km": "384400" }
Mixed types (agent parsing breaks randomly):
// Sometimes this:
{ "result": { "sun": { "sign": "Gemini" } } }
// Sometimes this:
{ "result": "No data available" }
Null without schema indication (agent does not know if null is valid):
{ "moon_sign": null, "sun_sign": "Gemini" }
The Correct Patterns
Numbers are numbers:
{ "longitude": 84.23, "distance_km": 384400 }
Consistent structure with explicit absent values:
{ "result": { "sun": { "sign": "Gemini" }, "moon": { "sign": null } } }
Schema declares nullability:
const chartSchema = z.object({
sun_sign: z.string(),
moon_sign: z.string().nullable(), // Explicitly nullable
ascendant: z.string().optional(), // May be absent entirely
});
Fixed Decimal Precision
For numerical APIs, use consistent decimal precision across all responses:
{
"longitude": 84.2341,
"latitude": -1.4502,
"speed": 0.9831,
"distance_au": 1.0142
}
Always 4 decimal places for coordinates, always 4 for speeds. Never 84.23 in one response and 84.234142857 in another. Consistent precision makes agent comparisons and caching reliable.
Pattern 3: Descriptive Enums Over Magic Values
When a field has a fixed set of possible values, use meaningful string enums rather than numeric codes or abbreviated strings.
Bad: Numeric Codes
{ "phase": 3, "house": 10, "dignity": 2 }
An agent seeing "phase": 3 cannot tell the user what moon phase this represents without a lookup table that may not be in context.
Bad: Abbreviated Strings
{ "sign": "Gem", "aspect": "cnj", "motion": "rx" }
Abbreviations save bytes but destroy agent interpretability. An agent might expand "rx" to "prescription" or "reactive extension" rather than "retrograde."
Good: Descriptive Enums
{
"phase": "waxing_gibbous",
"sign": "Gemini",
"aspect": "conjunction",
"motion": "retrograde"
}
Schema With Documented Enums
const aspectSchema = z.object({
type: z.enum([
"conjunction",
"opposition",
"trine",
"square",
"sextile",
"quincunx"
]).openapi({
description: "Type of angular aspect between two planets. Conjunction (0 degrees) indicates merged energies. Opposition (180 degrees) indicates tension and awareness. Trine (120 degrees) indicates harmony and flow."
}),
orb: z.number().openapi({
description: "Angular distance from exact aspect in decimal degrees. Smaller orb means stronger aspect influence.",
example: 2.45
}),
is_applying: z.boolean().openapi({
description: "Whether the aspect is forming (applying) or separating. Applying aspects are considered more potent in astrological interpretation."
}),
});
The enum values are self-documenting. The descriptions explain what each value means in context. An agent reading this schema can generate accurate natural language explanations without any external knowledge.
Pattern 4: Flat Over Deep (But Group When Logical)
Deeply nested JSON structures increase parsing complexity and token usage. Flat structures are easier for agents to process. But completely flat structures lose logical grouping that helps agents understand relationships.
Too Deep (agent loses track of nesting):
{
"chart": {
"planets": {
"inner": {
"sun": {
"position": {
"ecliptic": {
"longitude": {
"degrees": 84,
"minutes": 14,
"seconds": 2.76
}
}
}
}
}
}
}
}
Too Flat (agent cannot group related data):
{
"sun_longitude": 84.23,
"sun_latitude": 0.0002,
"sun_speed": 0.96,
"sun_sign": "Gemini",
"sun_house": 10,
"sun_retrograde": false,
"moon_longitude": 218.45,
"moon_latitude": 4.12,
"moon_speed": 13.2,
"moon_sign": "Scorpio",
"moon_house": 4,
"moon_retrograde": false
}
Just Right (one level of logical grouping):
{
"sun": {
"longitude": 84.23,
"sign": "Gemini",
"house": 10,
"speed": 0.96,
"is_retrograde": false
},
"moon": {
"longitude": 218.45,
"sign": "Scorpio",
"house": 4,
"speed": 13.2,
"is_retrograde": false
}
}
The Rule of Thumb
Maximum nesting depth of 2-3 levels. Group by logical entity (planet, house, aspect), not by data category (positions, signs, speeds). Every object at every level should have a clear semantic meaning that an agent can describe.
Pattern 5: Arrays of Objects, Not Objects of Arrays
When returning collections, use arrays of typed objects rather than parallel arrays or object-keyed collections.
Bad: Parallel Arrays (agent must zip by index):
{
"planet_names": ["Sun", "Moon", "Mercury"],
"longitudes": [84.23, 218.45, 72.1],
"signs": ["Gemini", "Scorpio", "Gemini"],
"retrograde": [false, false, true]
}
Bad: Dynamic Object Keys (agent cannot predict field names):
{
"Sun": { "longitude": 84.23 },
"Moon": { "longitude": 218.45 },
"Mercury": { "longitude": 72.1 }
}
Dynamic keys make OpenAPI documentation impossible because the field names are data-dependent. An agent cannot know in advance which keys will appear.
Good: Array of Typed Objects:
{
"planets": [
{ "name": "Sun", "longitude": 84.23, "sign": "Gemini", "is_retrograde": false },
{ "name": "Moon", "longitude": 218.45, "sign": "Scorpio", "is_retrograde": false },
{ "name": "Mercury", "longitude": 72.1, "sign": "Gemini", "is_retrograde": true }
]
}
Each item in the array has the same shape. The schema describes one object type. The agent can iterate, filter, and extract without special-case logic.
Pattern 6: ISO Standards for Dates, Times, and Units
Never invent custom formats for data that has an international standard.
Dates and Times
{
"date": "1990-06-15",
"time": "14:30:00",
"datetime": "1990-06-15T14:30:00Z",
"timezone_offset": 5.5
}
Always ISO 8601 for dates and times. Never "June 15, 1990" or "15/06/1990" or "06-15-1990". AI agents are trained on ISO 8601 and parse it reliably. Regional formats create ambiguity (is 01/02/2024 January 2nd or February 1st?).
Numerical Values With Units
Include the unit in the field name or in a companion field:
{
"distance_au": 1.0142,
"speed_degrees_per_day": 0.9831,
"orb_degrees": 2.45,
"altitude_meters": 0
}
Never return a bare number where the unit is ambiguous. Is 384400 in kilometers or miles? Is 13.2 in degrees per day or arc seconds per hour? Embedding the unit in the field name eliminates this class of agent misinterpretation.
Pattern 7: Pagination and Limits in Consistent Wrappers
When endpoints return paginated or limited collections, use a consistent wrapper:
{
"data": [
{ "name": "Sun", "longitude": 84.23 },
{ "name": "Moon", "longitude": 218.45 }
],
"total": 10,
"limit": 2,
"offset": 0
}
Or for cursor-based pagination:
{
"data": [...],
"next_cursor": "eyJpZCI6MTB9",
"has_more": true
}
The key is consistency: every paginated endpoint uses the same wrapper fields. An agent that learns to handle pagination on one endpoint can apply the same logic to all endpoints.
For non-paginated endpoints that return a single result (like a birth chart calculation), return the data directly without a wrapper. The distinction between "this is a collection" and "this is a single result" should be clear from the response structure.
Pattern 8: Error Responses That Match Success Schemas
Error responses should be as structured as success responses. An agent needs to distinguish errors from successes and extract actionable information from both.
Structured Error Pattern
{
"error": {
"code": "INVALID_PARAMETER",
"message": "The date parameter must be in YYYY-MM-DD format",
"parameter": "date",
"received": "June 15, 1990",
"expected_format": "YYYY-MM-DD",
"example": "1990-06-15"
}
}
This error response contains everything an agent needs to either fix the request automatically (it knows which parameter failed, what it received, and what it should send instead) or explain the problem clearly to the user.
What NOT to Do
{ "error": "Bad request" }
{ "message": "Something went wrong, please try again later" }
<html><body><h1>500 Internal Server Error</h1></body></html>
Vague errors, unstructured strings, and HTML error pages are the top three reasons agents fail at error recovery.
Putting It All Together: A Complete Example
Here is a well-designed API response that follows all eight patterns:
{
"planets": [
{
"name": "Sun",
"longitude": 84.2341,
"latitude": 0.0002,
"sign": "Gemini",
"sign_degree": 24.2341,
"house": 10,
"speed_degrees_per_day": 0.9583,
"is_retrograde": false,
"dignity": "peregrine"
},
{
"name": "Moon",
"longitude": 218.4512,
"latitude": 4.1203,
"sign": "Scorpio",
"sign_degree": 8.4512,
"house": 4,
"speed_degrees_per_day": 13.1762,
"is_retrograde": false,
"dignity": "fall"
}
],
"houses": [
{ "number": 1, "sign": "Virgo", "degree": 12.5531 },
{ "number": 2, "sign": "Libra", "degree": 8.1024 }
],
"aspects": [
{
"planet_1": "Sun",
"planet_2": "Moon",
"type": "trine",
"orb_degrees": 1.7829,
"is_applying": true
}
],
"metadata": {
"house_system": "placidus",
"zodiac_type": "tropical",
"date": "1990-06-15",
"time": "14:30:00",
"latitude": 40.7128,
"longitude": -74.006
}
}
Every field has a clear name. Types are consistent. Enums are descriptive. Nesting is logical but shallow. Arrays contain typed objects. Numbers use fixed precision. Dates follow ISO 8601. An agent can parse this response, understand every field, and generate accurate natural language summaries without any external documentation.
Validation: The Safety Net
Even with perfect schema design, validate responses before sending them. Schema validation catches bugs early and ensures agents always receive the structure they expect.
Zod (TypeScript)
import { z } from "zod";
const planetResponseSchema = z.object({
planets: z.array(z.object({
name: z.string(),
longitude: z.number().min(0).max(360),
sign: z.string(),
is_retrograde: z.boolean(),
})),
});
// Validate before sending response
const validated = planetResponseSchema.parse(rawData);
return c.json(validated);
Pydantic (Python)
from pydantic import BaseModel, Field
class PlanetResponse(BaseModel):
name: str
longitude: float = Field(ge=0, le=360)
sign: str
is_retrograde: bool
class ChartResponse(BaseModel):
planets: list[PlanetResponse]
# Validate before sending
response = ChartResponse(planets=raw_data)
return response.model_dump()
Runtime validation ensures that even if your calculation logic produces unexpected values, the API response always matches the documented schema. This is critical for agent trust: one malformed response can break an agent workflow permanently if the agent caches the schema mismatch.
For Developers: Seeing These Patterns in Production
RoxyAPI implements all eight patterns across 85+ endpoints covering Western astrology, Vedic astrology, tarot, numerology, dream interpretation, and I-Ching:
- Consistent snake_case naming across all response fields
- Typed responses validated with Zod schemas and served via OpenAPI
- Descriptive enums for signs, phases, aspects, and house systems
- Logical nesting (one level deep, grouped by entity)
- Arrays of typed objects for planets, houses, aspects, and card spreads
- ISO 8601 dates and unit-suffixed numerical fields
- Structured error codes with parameter-level detail
- Full field descriptions on every request and response field in the API documentation
These patterns work equally well for human developers reading the docs and AI agents consuming the API through MCP or direct HTTP calls. View pricing to get started.
Conclusion
AI-friendly JSON is not a different format. It is regular JSON designed with discipline: consistent naming, explicit types, descriptive enums, logical nesting, standardized formats, and runtime validation. These patterns improve the experience for every consumer of your API, whether human or machine.
The investment is minimal. Most patterns are enforced once in your schema definition (Zod, Pydantic, or OpenAPI) and apply automatically to every response. The payoff is an API that AI agents can consume reliably, accurately, and without brittle workarounds.
Key takeaways:
- Use consistent snake_case field names across all endpoints and response objects
- Every field should have exactly one type, always (never string-sometimes, number-sometimes)
- Use descriptive string enums instead of numeric codes or abbreviations
- Nest 2-3 levels deep maximum, grouped by logical entity
- Return arrays of typed objects rather than parallel arrays or dynamic-key objects
- Use ISO 8601 for all dates and times, and include units in numerical field names
- Structure error responses with machine-readable codes and parameter-level detail
- Validate all responses at runtime against your schema definitions
Ready to integrate with an API that follows these patterns? RoxyAPI provides 85+ endpoints across astrology, tarot, numerology, dreams, and I-Ching, all designed for both human developers and AI agents. Explore the documentation or check pricing to start building.
Frequently Asked Questions
Q: What is the most common JSON design mistake that breaks AI agent parsing?
A: Inconsistent field naming across endpoints. When the same concept is called birthDate in one endpoint, birth_date in another, and date_of_birth in a third, AI agents treat these as different fields and lose data continuity. Pick one naming convention (preferably snake_case) and enforce it everywhere.
Q: Should I use camelCase or snake_case for JSON API responses? A: For AI-friendly APIs, snake_case is recommended. It maps naturally to Python (the dominant language in AI/ML), is more readable as natural language (agents convert field names to human text), and avoids ambiguity with acronyms. Most major API design guides (Google, GitHub) also prefer snake_case for JSON.
Q: How deep should JSON nesting go for AI-friendly responses? A: Maximum 2-3 levels. One level of grouping by logical entity (planets, houses, aspects) is ideal. Deeper nesting increases token usage, makes agent parsing more complex, and risks losing data in context-limited models. If you find yourself nesting beyond 3 levels, flatten the structure or split into separate endpoints.
Q: Why should I avoid dynamic object keys in JSON responses?
A: Dynamic keys (where the field names are data-dependent, like using planet names as keys) cannot be documented in an OpenAPI schema. An agent has no way to know in advance which keys will appear. Array of objects with a typed name field is always preferable because the schema describes the exact shape of every item.
Q: How do I handle optional fields in AI-friendly JSON? A: Be explicit. In your schema, mark fields as nullable (value is null) or optional (field may be absent). Never mix the two semantics for the same field. In practice, nullable is safer than optional because the field is always present in the response, giving agents a consistent structure to parse regardless of whether the value is populated.
Q: What is the best way to include units in JSON numerical values?
A: Include the unit in the field name: distance_au, speed_degrees_per_day, altitude_meters. This eliminates ambiguity without requiring the agent to look up documentation. Avoid a separate "units" field because it introduces parsing complexity and the risk of unit/value mismatch.
Q: How does Zod compare to Pydantic for API schema definition? A: They serve the same purpose in different ecosystems. Zod is the standard for TypeScript/JavaScript APIs, providing runtime validation and JSON Schema generation from type definitions. Pydantic does the same for Python. Both generate OpenAPI-compatible schemas. Choose based on your backend language. For AI agent compatibility, what matters is the generated JSON Schema quality, not the tool that produced it.
Q: Do these patterns work for GraphQL APIs or only REST? A: These patterns apply to any API that returns JSON, including GraphQL. Consistent naming, explicit types, descriptive enums, and logical nesting improve GraphQL responses just as much as REST responses. The main difference is that GraphQL clients specify which fields they want, so the response shape is partially client-driven. But the underlying field definitions, types, and naming conventions still follow the same principles.