Remove a Field from an API
Remove stand pricing from the evolved API while preserving compatibility for applications that still depend on the earlier document shape.
Field removal is fundamentally different from additive evolution. The evolved API may no longer need a field even while the compatibility API still depends on it.
Business Step
Dave’s lemonade kiosk is growing into a fast-food chain model.
In the earlier sales API, stand_info included price_per_cup. That made sense when every stand behaved like a simple lemonade cart. As the business grows, pricing becomes a broader business concern. The newer sales API should describe the stand without exposing cup pricing as part of the stand object.
The challenge is compatibility. Existing clients still expect price_per_cup in stand_info, while newer clients should move forward without it.
Evolution Goal
This scenario demonstrates staged field removal. The design separates two decisions that are often confused: removing a field from the evolved API and removing it from the canonical storage model.
| Stage | Approach |
|---|---|
| Stage 1 | Keep the backing column in stands, but omit price_per_cup from sales_v2. Base-table defaults provide values for new rows inserted through the newer API. |
| Stage 2 | Remove price_per_cup from stands, then recreate the field for older clients with a generated compatibility value. |
sales_v1 remains the older compatibility API. sales_v2 is the newer API introduced by this scenario.
| Documentation view | Role |
|---|---|
sales_v1 |
Older compatibility API. Existing clients still see price_per_cup. |
sales_v2 |
Newer API. stand_info no longer exposes price_per_cup. |
Design Decision
Removing a field from an API and removing it from the canonical storage model are separate decisions, and staging them is what makes the change safe. Once the backing column is gone, the compatibility field can only be a constant, a derived value, or an ignored one.
Stage 1: Column retained, field removed from the new API
In the first stage, price_per_cup is removed only from the newer JSON API.
The backing column still exists in the stands table. The canonical storage model still stores price_per_cup, but sales_v2 no longer exposes it to newer clients.
Best Practice: Remove fields in stages. First remove the field from the evolved API while leaving the canonical storage model unchanged. Remove the stored value only after the compatibility API has an explicit replacement strategy.
Base Table Default
Because sales_v2 no longer sends price_per_cup, the base table must be able to provide a value when a new stand row is inserted through the newer view.
ALTER TABLE stands MODIFY (
price_per_cup DEFAULT 2.75 NOT NULL
);
The default belongs on the base table because the evolved API no longer carries the field. Compatibility is preserved by the canonical storage model, not by requiring newer clients to send obsolete data.
sales_v2 Without price_per_cup
sales_v2 removes price_per_cup from stand_info.
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW sales_v2 AS
sales @insert @update @delete
{
_id: sale_id,
date: sale_date,
quantity,
amount,
product_type,
tags,
customer_name,
email_address,
stand_info: stands @insert @update @nodelete
{
_id: stand_id,
location
}
};
The newer document shape is now smaller.
{
"_id": 7,
"date": "2026-01-17",
"quantity": 10,
"amount": 25.00,
"product_type": "Berry Lemonade",
"tags": "seasonal",
"customer_name": "talia",
"email_address": "talia@example.com",
"stand_info": {
"_id": 4,
"location": "CAMPUS CART, WEST QUAD"
}
}
What This Solves
sales_v2 gives newer clients the cleaner stand_info shape. Newer applications neither see nor send price_per_cup.
The canonical storage model still owns the column. If a new stand row is created through sales_v2, the table default supplies the missing value.
Case 1 Behavior
| API or schema object | Behavior |
|---|---|
sales_v1 |
Still exposes price_per_cup because older clients expect it. |
sales_v2 |
Does not expose price_per_cup. |
stands.price_per_cup |
Still exists in the relational schema. |
| Base table default | Supplies price_per_cup when sales_v2 inserts a stand without that field. |
Case 1 Limitation
This approach depends on the base column being nullable or having a default value. If price_per_cup is NOT NULL and has no default, inserts through sales_v2 will fail because the newer document no longer provides that field.
This stage removes the field only from the evolved API. The field still exists in the canonical storage model and remains available to sales_v1.
Stage 2: Column removed, old API remains compatible
The second stage completes the migration by removing the field from the canonical storage model.
The business no longer wants price_per_cup stored in the stands table. The column is physically removed from the relational schema.
ALTER TABLE stands DROP COLUMN price_per_cup;
After this point, sales_v2 continues to omit price_per_cup naturally because the evolved API no longer needs it.
The harder part is sales_v1. Existing applications still expect the field. Because the backing column is gone, the compatibility API must synthesize a replacement value.
sales_v1 With a Generated Compatibility Field
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW sales_v1 AS
sales @insert @update @delete
{
_id: sale_id,
date: sale_date,
quantity,
amount,
product_type,
tags,
customer_name,
email_address,
stand_info: stands @insert @update @nodelete
{
_id: stand_id,
location,
price_per_cup @generated(sql: "2.75")
}
};
The older document shape still includes price_per_cup.
{
"_id": 8,
"date": "2026-01-18",
"quantity": 4,
"amount": 11.00,
"product_type": "Ginger Lemonade",
"tags": "seasonal",
"customer_name": "sahil",
"email_address": "sahil@example.com",
"stand_info": {
"_id": 1,
"location": "MARKET CART, NORTH PLAZA",
"price_per_cup": 2.75
}
}
The field still exists in the older JSON API. It no longer exists as stored stand data.
What This Solves
Stage 2 completes the storage migration.
The evolved API has already moved on. The canonical storage model now moves on too. sales_v1 remains usable because price_per_cup is generated only for compatibility.
This is the key distinction between the two stages. In Stage 1, the field is absent from the evolved API but still stored. In Stage 2, the field is removed from storage and survives only as a compatibility value.
Case 2 Behavior
| API or schema object | Behavior |
|---|---|
sales_v1 |
Still exposes price_per_cup for older clients. |
sales_v2 |
Does not expose price_per_cup. |
stands.price_per_cup |
Removed from the relational schema. |
price_per_cup @generated |
Recreates the old field as compatibility output. |
Case 2 Limitation
After the backing column is dropped, price_per_cup becomes compatibility-only. Older clients may still send a value for this field, but generated fields are ignored in DML operations, so that value is no longer part of the canonical storage model.
A generated compatibility field is safe only when the old value can become constant, derived, or intentionally ignored. It is not safe when older clients still depend on distinct row-specific values.
New Behavior Used
This scenario introduces two compatibility mechanisms.
| Feature | Why it matters |
|---|---|
| Base table default | Supplies a value when sales_v2 inserts a stand without price_per_cup. |
@generated |
Recreates the removed field as compatibility output after the backing column has been removed. |
Evolution Check
By the end of this scenario, the evolution proves the following:
sales_v1continues to support the olderstand_infoshape withprice_per_cup.sales_v2supports the newerstand_infoshape withoutprice_per_cup.- The field can first be hidden from the newer API while the backing column remains.
- The backing column can later be removed if
sales_v1has a generated compatibility value. - Newer systems are not forced to carry the old pricing field.
- Older systems are not broken just because the newer API is cleaner.
Topic Takeaway
This scenario demonstrates that removing a field from an API and removing it from the canonical storage model are separate design decisions.
The safer first move is to remove the field from the evolved API while the relational column still exists. That gives newer clients the correct shape without forcing immediate storage cleanup.
The deeper move is to remove the backing column. At that point, the compatibility API needs an explicit replacement strategy. In this scenario, sales_v1 preserves price_per_cup with @generated, while sales_v2 moves forward without exposing the field.
The important design principle is to separate API retirement from storage retirement. Remove a field from storage only after its compatibility behavior is understood and intentionally designed.
The cost of the second stage is permanent: once the backing column is gone, the compatibility field can only be a constant, a derived value, or one that is ignored. It cannot return row-specific stored data, so this path is closed to old clients that still depend on distinct per-row values.