jsondata-modelingapibest-practicesschema-design

JSON Data Modeling: Designing Schemas for Real-World Applications

·10 min read

What is JSON Data Modeling?

JSON data modeling is the practice of deliberately designing the shape of your JSON objects — choosing which fields to include, how to nest them, when to use arrays vs objects, and how to handle relationships between data.

A well-designed JSON schema is intuitive, consistent, and easy to extend. Use JSONKit's JSON Schema Generator to auto-generate a schema from any existing JSON object. A poorly designed schema forces consumers to work around it for every version of the API.

Principle 1: Flat is Better than Nested (Until It Isn't)

Nesting adds hierarchy and context but makes consumption harder. Start flat:

json
// Too flat — no grouping, hard to extend
{
  "userId": 1,
  "firstName": "Ravi",
  "city": "Surat",
  "postalCode": "395007",
  "countryCode": "IN"
}
json
// Appropriately nested — address is a logical unit
{
  "id": 1,
  "firstName": "Ravi",
  "address": {
    "city": "Surat",
    "postalCode": "395007",
    "countryCode": "IN"
  }
}
json
// Over-nested — unnecessary depth
{
  "user": {
    "personal": {
      "identity": {
        "name": { "first": "Ravi" }
      }
    }
  }
}

Rule: Group fields that are always used together and that logically belong together. Avoid nesting for the sake of namespace isolation.

Principle 2: Arrays of Objects for Collections

Use arrays of objects, not objects keyed by IDs, for collections:

json
// Avoid — object keyed by ID is hard to iterate and append
{
  "users": {
    "1": { "name": "Ravi" },
    "2": { "name": "Priya" }
  }
}
json
// Prefer — array of objects; easy to map, filter, iterate
{
  "users": [
    { "id": 1, "name": "Ravi" },
    { "id": 2, "name": "Priya" }
  ]
}

Exceptions: use keyed objects when: - You need O(1) lookup by a key (a lookup table / dictionary) - The dataset is a fixed, small enumeration (e.g., status codes, config flags)

Principle 3: IDs for References, Not Full Embedding

Embed a related object when: - It is always needed together with the parent - It is small and does not change independently - Fetch performance outweighs duplication

Use an ID reference when: - The related object is large - It changes independently and needs its own cache entry - Multiple parent objects reference the same child

json
// Embedded (user always comes with their address)
{
  "id": "ord_123",
  "customer": {
    "id": "usr_456",
    "name": "Ravi",
    "email": "ravi@example.com"
  },
  "total": 1299.00
}
json
// ID reference (load product details on demand)
{
  "id": "ord_123",
  "customerId": "usr_456",
  "lineItems": [
    { "productId": "prod_789", "quantity": 2, "unitPrice": 599.00 }
  ],
  "total": 1298.00
}

Principle 4: Consistent Date Format (ISO 8601)

Always use ISO 8601 UTC strings for dates and timestamps:

json
{
  "createdAt": "2025-06-01T12:30:00Z",
  "expiresAt": "2025-12-31T23:59:59Z",
  "date":      "2025-06-01"
}

Never use Unix timestamps as numbers in public APIs — they are not human-readable and cause timezone confusion. Never use locale-formatted strings like "01/06/2025" — they are ambiguous between MM/DD and DD/MM.

Principle 5: Use null for Absent Optional Values

Return null for optional fields that have no value rather than omitting the field entirely:

json
// Inconsistent — sometimes middleName is absent, sometimes null
{ "name": "Ravi" }
{ "name": "Priya", "middleName": null }
json
// Consistent — always present, null when absent
{ "name": "Ravi",  "middleName": null }
{ "name": "Priya", "middleName": "Kumari" }

This makes consumers' code simpler — they always check if (user.middleName !== null) rather than if ("middleName" in user).

Principle 6: Naming Conventions

Pick one convention and use it everywhere:

  • camelCase (most common for JavaScript APIs): firstName, createdAt, isActive
  • snake_case (common in Python, Ruby APIs): first_name, created_at, is_active
  • PascalCase (avoid in JSON — too similar to class names)
  • kebab-case (avoid in JSON keys — hyphens require bracket notation in JS)

Boolean fields: prefix with is, has, or can:

json
{
  "isActive": true,
  "hasSubscription": false,
  "canEdit": true
}

Principle 7: Envelope Responses for APIs

Wrap API responses in a consistent envelope so clients can always extract data the same way:

json
{
  "success": true,
  "data": { "id": 1, "name": "Ravi" },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2025-06-01T12:30:00Z"
  }
}
json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The 'email' field is required.",
    "field": "email"
  }
}

This pattern (RFC 7807 Problem Details is a formal version) lets clients write a single error handler instead of a special case per endpoint.

Principle 8: Design for Forward Compatibility

Design your JSON to be extendable without breaking existing consumers:

  • Add new optional fields — old consumers ignore unknown keys by default in most languages
  • Never remove or rename existing fields — this is a breaking change
  • Never change a field's type (string → number) — always breaking
  • Use a version field or URL versioning when you must make breaking changes:
json
{ "version": "2", "data": { ... } }

Or use API versioning at the URL level: /api/v2/users.

Common JSON Schema Patterns

Status enum:

json
{ "status": "pending" }

Use string enums, not integers. "status": "active" is self-documenting; "status": 2 is not.

Money:

json
{ "amount": 1299, "currency": "INR" }

Store as integer cents/paise with an explicit currency code — never as floating-point numbers.

Pagination:

json
{
  "data": [...],
  "pagination": {
    "page": 2, "limit": 20, "total": 847,
    "nextCursor": "eyJpZCI6MjB9"
  }
}

Error response (RFC 7807):

json
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation Error",
  "status": 422,
  "detail": "The 'email' field must be a valid email address.",
  "instance": "/api/users",
  "errors": [
    { "field": "email", "message": "Invalid email format" }
  ]
}

Generate a JSON Schema from any of these patterns using JSONKit's JSON Schema Generator at /json-schema-generator, then validate incoming data against it with the JSON Schema Validator at /json-schema-validator.

Try JSON Schema Generator

Generate a JSON Schema from your data model to enforce structure automatically.