DEV Community

snehawani
snehawani

Posted on

Schema Evolution in Financial Data APIs: Versioning Without Breaking Consumers

A financial data API can work perfectly today and still break a downstream service tomorrow.

Imagine an API that returns a credit account as JSON:

{
  "account_id": "acc_1024",
  "balance": "12500.00",
  "currency": "INR",
  "status": "active"
}
Enter fullscreen mode Exit fullscreen mode

A consumer service reads balance, converts it into a decimal, and generates a monthly summary.

Later, the API team decides to improve the response. Perhaps balance becomes current_balance, a new status is introduced, or the response begins including nullable fields.

The change may look harmless from the API team's perspective. But if a consumer expects the original field, a deployment can suddenly fail or produce incomplete results.

This is a schema evolution problem.

In financial systems, schema changes deserve particular care because data can move through reporting services, analytics pipelines, reconciliation jobs, customer dashboards, and audit systems.

The goal is not to prevent change. It is to make change predictable.

1. Start by separating additive and breaking changes

Not all schema changes have the same risk.

Consider these examples:

Change Typical compatibility risk
Adding an optional field Low, if consumers tolerate unknown fields
Adding a required field High for existing producers and consumers
Renaming a field High
Removing a field High
Changing a field's type High
Changing units or meaning Potentially very high
Adding a new enum value Risky for consumers with exhaustive enum handling

The last two cases are easy to underestimate.

Changing a monetary value from rupees to paise while keeping the same field name is a semantic breaking change, even if the JSON type remains a string.

Similarly, a consumer that assumes status can only be "active" or "closed" may fail when the producer adds "suspended".

Compatibility is about meaning and behaviour, not just whether the JSON still parses.

2. Define an explicit contract

A schema contract should describe more than field names and data types.

For a financial API, document:

  • Required and optional fields
  • Nullability rules
  • Numeric precision and representation
  • Currency and unit conventions
  • Allowed status values
  • Timestamp formats and time zones
  • Meaning of missing, unknown, and zero values
  • Deprecation and removal policies

For example, representing money as a decimal string avoids accidental floating-point conversion in clients that use binary floating-point arithmetic.

{
  "balance": "12500.00",
  "currency": "INR"
}
Enter fullscreen mode Exit fullscreen mode

This representation still needs a documented precision and rounding policy. A string alone does not guarantee correct financial arithmetic.

In Python, use Decimal when performing decimal arithmetic:

from decimal import Decimal

balance = Decimal("12500.00")
payment = Decimal("1500.25")

remaining = balance - payment

print(remaining)  # 10999.75
Enter fullscreen mode Exit fullscreen mode

Avoid constructing a Decimal from an already imprecise float when exact decimal input is available.

3. Add fields before requiring consumers to use them

Suppose the original API exposes balance, and the team wants to introduce current_balance.

A safer migration has several stages.

Stage A: Add the new field.

{
  "balance": "12500.00",
  "current_balance": "12500.00",
  "currency": "INR"
}
Enter fullscreen mode Exit fullscreen mode

Both fields should represent the same documented value during the migration.

Stage B: Update consumers.

Deploy consumer code that can read current_balance while remaining compatible with responses that still contain only balance, if that fallback is part of the migration contract.

Stage C: Measure usage.

Use logs, metrics, or consumer-level telemetry to identify whether any active clients still depend on the old field.

Stage D: Deprecate the old field.

Publish a deprecation notice and communicate the removal timeline to affected consumers.

Stage E: Remove the old field only after the compatibility window.

The exact timeline depends on the API's consumers, release process, and operational requirements.

This approach avoids making every service migrate at the same instant.

4. Treat missing, null, and zero as different states

Financial data often needs careful handling of absent values.

Consider three responses:

{
  "minimum_due": "0.00"
}
Enter fullscreen mode Exit fullscreen mode
{
  "minimum_due": null
}
Enter fullscreen mode Exit fullscreen mode
{}
Enter fullscreen mode Exit fullscreen mode

These may mean different things:

  • "0.00": the known minimum due is zero.
  • null: the field is present, but the value is unknown or unavailable under the contract.
  • Missing field: the producer did not provide the field, perhaps because it is optional or the schema version differs.

Never assume these states are interchangeable.

A consumer that converts missing values to zero can accidentally transform incomplete data into a valid-looking financial result.

Define the semantics explicitly, and test each state.

5. Use contract tests to catch incompatible changes

Schema documentation is helpful, but automated checks make compatibility more reliable.

A simple Pydantic model can validate the structure of an incoming response:

from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict

class CreditAccount(BaseModel):
    model_config = ConfigDict(extra="ignore")

    account_id: str
    balance: Decimal
    currency: str
    status: Literal[
        "active",
        "closed",
        "suspended"
    ]
Enter fullscreen mode Exit fullscreen mode

This model is intentionally strict about required fields and recognised status values while ignoring unknown additional fields.

That is one possible consumer policy, not a universal choice. Some systems should reject unknown enum values; others should preserve them for forward compatibility. Decide based on the risk and expected behaviour of the application.

A useful test suite should include:

  1. A valid response from the current producer.
  2. A response with an optional field omitted.
  3. A response with an additional unknown field.
  4. A response with a missing required field.
  5. A response containing null where null is not permitted.
  6. A response with an unsupported status.
  7. A response with invalid monetary precision or formatting.

For stronger guarantees, validate new producer schemas against representative payloads from older versions and run consumer-driven contract tests where multiple services depend on the API.

6. Test semantic compatibility, not just structural compatibility

Two schemas can look identical while representing different things.

For example, a field named balance could mean:

  • The current outstanding balance
  • The statement balance
  • The amount due on the next payment date
  • The balance as of a previous reporting date

All four values could be decimal strings. A structural schema validator may accept each one.

That does not make them interchangeable.

Document the business meaning, effective date, source, and calculation rules for fields that influence financial decisions.

When a definition changes, treat it as a contract change even if the field name and data type remain the same.

7. Version intentionally

There are several common approaches to API versioning.

URL versioning

/api/v1/accounts
/api/v2/accounts
Enter fullscreen mode Exit fullscreen mode

This makes major versions visible and easy to route, but maintaining multiple versions can increase operational complexity.

Media-type or header versioning

A client specifies the contract version through request headers. This keeps URLs stable but can make debugging less obvious if version information is not logged clearly.

Additive evolution within one version

Compatible changes are introduced without creating a new major version. Breaking changes require a separate migration strategy.

For many systems, a practical rule is to allow backward-compatible additions within a version and use a documented process for incompatible changes.

The important part is consistency. Versioning should describe a real contract boundary, not merely a deployment number.

8. Monitor deprecations before removing old fields

A deprecation announcement does not prove that a field is unused.

Track relevant signals such as:

  • Requests made by client version
  • Use of deprecated fields, where observable
  • Contract-test failures
  • Error rates after deployments
  • Consumer migration progress
  • Unexpected null or parsing rates

Avoid logging sensitive financial payloads merely to observe field usage. Prefer low-risk metadata and aggregate metrics, and apply appropriate access controls and retention limits.

If a consumer cannot be identified or usage cannot be measured reliably, use a more conservative removal plan.

9. Keep a schema migration checklist

Before releasing a schema change, ask:

  • Is the change structural, semantic, or both?
  • Will existing consumers continue to work?
  • Are missing and null values handled correctly?
  • Have older payloads been tested?
  • Are money and timestamp conventions documented?
  • Is a migration or rollback plan available?
  • Are consumers aware of deprecation dates?
  • Can usage be monitored without exposing sensitive data?
  • Are schema and contract tests part of CI?

This checklist is deliberately simple. Its purpose is to make compatibility part of the normal release process rather than an afterthought.

Conclusion

Financial APIs evolve as products, reporting requirements, and downstream systems change. The safest approach is to make those changes explicit.

Define the contract, distinguish structural changes from semantic changes, introduce replacements before removing old fields, and test the behaviour of real consumers.

A schema is not just a description of JSON. It is an agreement between the systems that produce data and the systems that depend on it.

When that agreement is treated as part of the software architecture, teams can evolve their APIs without turning every change into a production incident.


AI assistance disclosure: This draft was prepared with AI assistance. Review, verify, and revise it with your own technical experience before publication, and follow DEV Community's current AI-assisted article guidelines.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.