← Back to Blog

Advanced JSON Structures and Design Patterns

Master advanced JSON design patterns. Learn normalization, polymorphism, versioning, and API response patterns for scalable applications.

Sarah Chen12 min readadvanced
S

Sarah Chen

Senior Software Engineer

Sarah is a full-stack software engineer with 8 years of experience in API development, TypeScript, and data engineering. She has designed and maintained large-scale JSON processing pipelines and contributes in-depth technical guides on performance optimisation, schema design, Python data workflows, and backend integration patterns.

TypeScriptAPI DevelopmentPythonData EngineeringJSON SchemaPerformance Tuning
12 min read

Nested vs Flat Structures

{

"id": 1,

"name": "John",

"address": {

"street": "123 Main St",

"city": "NYC"

},

"contact": {

"email": "john@example.com",

"phone": "+1234567890"

}

}

Avoid excessive nesting (>3-4 levels).

Normalization

Denormalized (Embedded)

{

"orders": [

{

"id": 1,

"customer": {

"id": 101,

"name": "Alice"

}

}

]

}

Use when data is read frequently, updated rarely.

Normalized (References)

{

"orders": [

{ "id": 1, "customerId": 101 }

],

"customers": {

"101": { "id": 101, "name": "Alice" }

}

}

Use when data changes frequently.

API Response Patterns

Envelope Pattern

{

"success": true,

"data": { ... },

"meta": {

"total": 100,

"page": 1

}

}

Error Response

{

"success": false,

"error": {

"code": "VALIDATION_ERROR",

"message": "Invalid input",

"details": [

{

"field": "email",

"message": "Invalid format"

}

]

}

}

Pagination Patterns

Offset-Based Pagination

{

"data": [

{"id": 21, "name": "Item 21"},

{"id": 22, "name": "Item 22"}

],

"pagination": {

"page": 2,

"perPage": 20,

"total": 156,

"totalPages": 8

},

"links": {

"first": "/api/items?page=1",

"prev": "/api/items?page=1",

"next": "/api/items?page=3",

"last": "/api/items?page=8"

}

}

Pros: Easy to implement, jump to specific page Cons: Performance degrades with deep pagination, inconsistent results if data changes
{

"data": [{"id": 100, "name": "Item 100", "created_at": "2026-01-15T10:30:00Z"}],

"pagination": {

"cursor": "eyJpZCI6MTAwLCJjcmVhdGVkX2F0IjoiMjAyNi0wMS0xNVQxMDozMDowMFoifQ==",

"hasMore": true

},

"links": {

"next": "/api/items?cursor=eyJpZCI6MTAwLCJjcmVhdGVkX2F0IjoiMjAyNi0wMS0xNVQxMDozMDowMFoifQ=="

}

}

Pros: Consistent results, O(1) performance, handles real-time data Cons: Can't jump to specific page

Implementation Example

// Express.js cursor pagination

app.get('/api/items', async (req, res) => {

const { cursor, limit = 20 } = req.query;

let query = db('items').orderBy('created_at', 'desc').limit(limit + 1);

if (cursor) {

const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());

query = query.where('created_at', '<', decoded.created_at)

.orWhere(function() {

this.where('created_at', '=', decoded.created_at)

.where('id', '<', decoded.id);

});

}

const items = await query;

const hasMore = items.length > limit;

const data = items.slice(0, limit);

const nextCursor = hasMore

? Buffer.from(JSON.stringify({

id: data[data.length - 1].id,

created_at: data[data.length - 1].created_at

})).toString('base64')

: null;

res.json({

data,

pagination: { cursor: nextCursor, hasMore }

});

});

Polymorphic Data

Discriminator Pattern

{

"notifications": [

{

"type": "email",

"recipient": "user@example.com",

"subject": "Welcome!",

"body": "Thanks for signing up"

},

{

"type": "sms",

"phoneNumber": "+1234567890",

"message": "Your verification code is 123456"

},

{

"type": "push",

"deviceToken": "abc123",

"title": "New message",

"body": "You have 3 unread messages"

}

]

}

TypeScript Type Guards

type EmailNotification = {

type: 'email';

recipient: string;

subject: string;

body: string;

};

type SmsNotification = {

type: 'sms';

phoneNumber: string;

message: string;

};

type Notification = EmailNotification | SmsNotification;

function sendNotification(notification: Notification) {

switch (notification.type) {

case 'email':

// TypeScript knows this is EmailNotification

sendEmail(notification.recipient, notification.subject);

break;

case 'sms':

// TypeScript knows this is SmsNotification

sendSms(notification.phoneNumber, notification.message);

break;

}

}

Versioning Strategies

URL Versioning

GET /api/v1/users

GET /api/v2/users # Breaking changes

Header Versioning

GET /api/users

Accept: application/vnd.myapi.v2+json

Content Versioning (In Payload)

{

"version": "2.0",

"data": {

"id": 1,

"fullName": "John Doe", // v1 had firstName/lastName

"email": "john@example.com"

}

}

Backward Compatibility Example

// API handler supports both v1 and v2

function formatUserResponse(user, version = '2.0') {

if (version === '1.0') {

return {

id: user.id,

firstName: user.fullName.split(' ')[0],

lastName: user.fullName.split(' ')[1] || '',

email: user.email

};

}

return {

version: '2.0',

id: user.id,

fullName: user.fullName,

email: user.email

};

}

Event/Message Patterns

CloudEvents Standard

{

"specversion": "1.0",

"type": "com.example.user.created",

"source": "https://api.example.com/users",

"id": "evt_abc123",

"time": "2026-01-15T10:30:00Z",

"datacontenttype": "application/json",

"data": {

"userId": 123,

"email": "newuser@example.com"

}

}

Message Queue Pattern

{

"messageId": "msg_xyz789",

"timestamp": "2026-01-15T10:30:00Z",

"eventType": "order.placed",

"version": "1.0",

"payload": {

"orderId": 12345,

"customerId": 67890,

"total": 99.99

},

"metadata": {

"source": "web-checkout",

"userId": 67890,

"sessionId": "sess_abc123"

}

}

Best Practices

Naming Conventions

Be consistent across your API:

// camelCase (JavaScript/TypeScript)

{"userId": 1, "firstName": "John", "createdAt": "2026-01-15"}

// snake_case (Python/Ruby)

{"user_id": 1, "first_name": "John", "created_at": "2026-01-15"}

Date/Time (ISO 8601)

{

"createdAt": "2026-01-15T10:30:00Z", // UTC

"updatedAt": "2026-01-15T10:30:00-05:00", // With timezone

"date": "2026-01-15" // Date only

}

Money/Currency (Avoid Floats)

{

"price": {

"amount": 2999, // Store in cents/smallest unit

"currency": "USD"

},

"total": {

"amount": 15495,

"currency": "EUR"

}

}

Boolean Flags

{

"isActive": true,

"hasPermission": false,

"isDeleted": false

}

Avoid: "status": "active" when a boolean suffices.

HATEOAS (Hypermedia)

{

"id": 1,

"name": "Laptop",

"price": 999,

"_links": {

"self": {

"href": "/api/products/1"

},

"update": {

"href": "/api/products/1",

"method": "PUT"

},

"delete": {

"href": "/api/products/1",

"method": "DELETE"

},

"reviews": {

"href": "/api/products/1/reviews"

}

}

}

Bulk Operations

{

"operations": [

{

"op": "create",

"type": "user",

"data": {"name": "Alice", "email": "alice@example.com"}

},

{

"op": "update",

"type": "user",

"id": 123,

"data": {"name": "Alice Smith"}

},

{

"op": "delete",

"type": "user",

"id": 456

}

]

}

Response Format

{

"results": [

{"op": "create", "success": true, "id": 789},

{"op": "update", "success": true},

{"op": "delete", "success": false, "error": "User not found"}

],

"summary": {

"total": 3,

"succeeded": 2,

"failed": 1

}

}

Sparse Fieldsets (JSON:API Style)

Reduce payload size by requesting only needed fields:

GET /api/users?fields[user]=id,name,email
{

"data": [

{"id": 1, "name": "Alice", "email": "alice@example.com"},

{"id": 2, "name": "Bob", "email": "bob@example.com"}

]

}

Vs. full response (40% bandwidth savings).

GraphQL-Style Nested Queries

{

"query": {

"user": {

"id": 1,

"include": ["posts", "comments"],

"posts": {

"limit": 5,

"include": ["author"]

}

}

}

}

Production Design Checklist

☑️ Use cursor pagination for infinite scroll

☑️ Include version field for schema evolution

☑️ Use discriminator (type field) for polymorphic data

☑️ Store money in cents/smallest unit

☑️ Use ISO 8601 for dates with timezone

☑️ Include error codes in error responses

☑️ Provide HATEOAS links for discoverability

☑️ Support sparse fieldsets to reduce bandwidth

☑️ Use envelope pattern for consistent response structure

☑️ Add request IDs for tracing and debugging

Conclusion

Advanced JSON design patterns enable scalable, maintainable APIs. Use cursor pagination for performance, discriminator pattern for polymorphism, ISO 8601 dates for consistency, and cents for currency to avoid floating-point errors. Always include version fields and error codes to support evolution and debugging in production systems.

{ "id": 1, "name": "Alice", "email": "alice@example.com" }

]

}

```

Conclusion

Good JSON structure design:

  • Balance nesting appropriately
  • Normalize based on read/write patterns
  • Use consistent patterns
  • Version your data
  • Follow naming conventions
  • Well-structured JSON makes APIs easier to use and maintain!

    Share:

    Related Articles