Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Canary Deployments with Progressive JSON Updates

Deploying new software versions is a critical part of the development lifecycle, but it always carries risk. How do you minimize the chance that a new release introduces bugs, performance regressions, or breaking changes that impact a significant number of users?

One widely adopted strategy is the Canary Deployment.

What are Canary Deployments?

A canary deployment is a technique that rolls out a new version of an application or service to a small subset of users or servers first. It's like sending a "canary in a coal mine" – if something goes wrong, only a small number of users are affected, and you can quickly detect the issue and roll back before it impacts everyone.

Typically, this involves running the new version (the "canary") alongside the old version in production. A load balancer, API gateway, or service mesh directs a small percentage of traffic (e.g., 1-5%) to the canary instances. The operations team then monitors key metrics – error rates, latency, resource usage, business KPIs – from the canary. If the metrics look good after a period, traffic is gradually shifted to the new version until 100% of users are on it. If metrics are bad, traffic is immediately routed back to the old version, and the canary is rolled back.

Gradual traffic shift is key.

The Challenge: Data Structure Changes

While rolling out code changes gradually is handled by traffic splitting, what happens when your deployment includes changes to data structures? This is especially common in APIs or services that exchange data using formats like JSON.

Imagine you're updating an API endpoint that returns user data. The old version might return:

Old JSON Structure:

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

Your new version adds a `phone` field and changes `email` to `contactEmail`:

New JSON Structure:

{
  "id": "user123",
  "name": "Alice",
  "contactEmail": "alice@example.com",
  "phone": "123-456-7890"
}

During a canary deployment, both the old and new versions of your service are running simultaneously. Clients (frontend apps, mobile apps, other services) might receive responses from *either* version.

  • An old client (expecting the old JSON) might receive the new JSON.
  • A new client (possibly also in a canary rollout, or a frontend coupled to the new backend) might receive the old JSON.

If the old client cannot handle the new JSON structure (e.g., due to strict parsing, expecting `email` but getting `contactEmail`, or choking on unexpected fields), it will break. Similarly, if the new client relies on `contactEmail` or `phone` and receives the old JSON, it will also break.

Introducing Progressive JSON Updates

To address this, we need a strategy to make JSON data structure changes compatible across versions running concurrently during a progressive rollout. This is where the concept of Progressive JSON Updates comes in. It involves designing your API responses and the code that handles them so that both older and newer versions of your application code can gracefully process the JSON data being served by any concurrently running service version.

The goal is to ensure that during the transition period of a canary or phased rollout, clients receive data they can work with, regardless of which service version served it.

Strategy 1: Optional Fields & Backwards Compatibility

The simplest approach is to make new fields optional and ensure older versions of your code gracefully ignore them. New versions of your code must be written to handle the *absence* of new fields if they receive data from an older service instance.

Adding Fields:

  • New service returns JSON with the new field.
  • Old client code receives this, ignores the new field (most JSON parsers are forgiving and allow extra fields).
  • New service code receives old JSON (missing the new field). It must check for the field's existence and handle the case where it's missing.

Conceptual New Service Code (handling old JSON):

interface UserDataV1 {
  id: string;
  name: string;
  email: string; // Old field name
}

interface UserDataV2 {
  id: string;
  name: string;
  contactEmail: string; // New field name
  phone?: string;      // New, optional field
}

// Function in the New Service Code that processes user data
function processUserData(data: UserDataV1 | UserDataV2): void {
  const userId = data.id;
  const userName = data.name;

  // Handle potentially missing or renamed fields
  let userEmail: string;
  if ((data as UserDataV2).contactEmail !== undefined) {
    // Received V2 data
    userEmail = (data as UserDataV2).contactEmail;
    const userPhone = (data as UserDataV2).phone; // Will be undefined for V1 data
    console.log(`Processing V2 data for ${userName}: Email=${userEmail}, Phone=${userPhone ?? 'N/A'}`);
  } else if ((data as UserDataV1).email !== undefined) {
    // Received V1 data
    userEmail = (data as UserDataV1).email;
    console.log(`Processing V1 data for ${userName}: Email=${userEmail}`);
  } else {
     throw new Error("Unknown data format");
  }

  // ... rest of processing logic ...
}

// Example usage:
// const oldData = { id: "user1", name: "Bob", email: "bob@example.com" };
// const newData = { id: "user2", name: "Charlie", contactEmail: "charlie@example.com", phone: "987-654-3210" };
// processUserData(oldData); // New code handles old data
// processUserData(newData); // New code handles new data

Renaming/Removing Fields: This is harder.

  • Renaming (`email` to `contactEmail`): The new service should ideally return *both* fields for a transition period, with the new one being the primary source of truth. Old clients use `email`, new clients use `contactEmail`. Eventually, after the old service version is fully decommissioned and all clients are updated, the old field can be removed.

    JSON during Renaming Transition:

    {
      "id": "user123",
      "name": "Alice",
      "email": "alice@example.com",      // Old field (kept for compatibility)
      "contactEmail": "alice@example.com", // New field
      "phone": "123-456-7890"
    }
  • Removing Fields: A field cannot be abruptly removed if old clients still expect it. This usually requires a phased approach where the field is first deprecated (marked as optional, clients warned), then removed only after all consumers are confirmed to no longer use it.

Strategy 2: Transformation Layer

A more robust, but complex, approach is to introduce a layer that transforms the JSON response based on the client or the service version it originated from. This layer could be:

  • An API Gateway or Proxy that inspects headers (e.g., `Accept` header specifying a version, or a custom canary header) and rewrites the JSON structure on the fly.
  • Logic within the service itself that detects the client's expected version (e.g., via a query parameter or header) and formats the JSON accordingly.

This allows the backend service to primarily work with its internal, newer data model while presenting an older model to clients who need it.

Conceptual Transformation Logic (within Gateway or Service):

// Assume 'internalData' is the latest version (UserDataV2)
const internalData = {
  id: "user123",
  name: "Alice",
  contactEmail: "alice@example.com",
  phone: "123-456-7890"
};

// Assume 'clientExpectsVersion' is determined from request (e.g., header)
const clientExpectsVersion = req.headers['x-api-version'] || 'v2'; // Default to latest

let responseData;
if (clientExpectsVersion === 'v1') {
  // Transform V2 data to V1 format
  responseData = {
    id: internalData.id,
    name: internalData.name,
    email: internalData.contactEmail // Map new field back to old name
    // 'phone' field is omitted for V1 clients
  };
} else {
  // Client expects V2 or higher, return latest format
  responseData = internalData;
}

// Send responseData
// res.json(responseData);

This strategy requires careful versioning and maintenance of transformation logic, but provides greater control over the data contract presented to different clients during migration.

Combining Canaries and Progressive JSON

When you combine canary deployments with progressive JSON update strategies, you significantly de-risk rollouts involving schema changes.

  • The canary deployment handles the gradual rollout of the new code.
  • The progressive JSON strategy handles the compatibility of the data format across concurrent code versions.

During the canary phase, a small percentage of users interact with the new code, which is designed to handle both old and new data formats. If data compatibility issues arise (e.g., a bug in the new code when processing old data, or an old client failing unexpectedly when receiving new data due to strictness), they only affect the small canary group and can be detected via monitoring. You can then roll back the code canary and address the data compatibility issue.

As the canary traffic percentage increases, you gain more confidence that the data compatibility strategy holds up under real-world load and diverse client types.

Challenges

  • Complexity: Implementing robust backwards and forwards compatibility in your data handling code adds complexity. Every field change needs careful consideration.
  • Testing: You must rigorously test that both old and new code versions can handle both old and new data formats.
  • Maintainability: Transformation logic or conditional data handling can become cumbersome over time, especially with many versions or frequent schema changes.
  • Removing Data: Safely removing fields requires multi-step processes, often spanning multiple deployments and monitoring cycles.

Conclusion

Canary deployments are essential for reducing the risk of code rollouts. However, for systems dealing with structured data like JSON APIs, code rollouts are often coupled with data structure evolution. Progressive JSON updates provide the necessary data compatibility layer that allows different versions of your application to coexist and safely handle varying data formats during the transition period of a canary deployment. By carefully designing your data structures for compatibility and implementing robust handling in your code, you can achieve smoother, safer deployments even as your data model evolves.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool