Adding Required Request Fields Safely

This page belongs to Breaking Change Detection in the API Versioning & Deprecation reference. Making a request field required is the most common intentional break in a maturing API — a field that “should always have been there” now needs enforcing. It is also the one with the cleanest staged migration, provided you start before the enforcement rather than at it.

The decision trigger

Situation What it means
A field is present in 98% of requests and the code assumes it you want to enforce, and can stage it
Downstream systems need a value the API never collected you need a default, or a new operation
A validation rule exists in code but not in the schema the spec understates the requirement — fix the spec first
A required field is needed for regulatory reasons on a date enforcement date is fixed; stage backwards from it
No sensible default exists for existing callers this cannot be staged — version it

Spec snippet: the three states

# Stage 1 — optional, with a documented server-side default
components:
  schemas:
    OrderCreate:
      type: object
      required: [items]                     # `channel` deliberately absent
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/OrderItem" } }
        channel:
          type: string
          enum: [web, mobile, partner, internal]
          description: >
            Origination channel. Optional until 2026-11-30, after which it is
            REQUIRED. Omitting it currently defaults to `web` and returns a
            deprecation warning in the response.

# Stage 3 — required, after the announced date
components:
  schemas:
    OrderCreate:
      type: object
      required: [items, channel]            # enforced
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/OrderItem" } }
        channel:
          type: string
          enum: [web, mobile, partner, internal]
          description: Origination channel. Required since 2026-11-30.

The description in stage 1 carries the whole plan: what the default is, when it stops applying, and that a warning is being emitted. A reader of the specification learns everything they need without finding a changelog entry.

Staging a new required field Stage one ships the field as optional with a default and no visible change for clients. Stage two adds warnings and measures which callers still omit it. Stage three enforces the requirement and returns a validation error naming the field. 1 · optional + default server substitutes `web` no client change needed diff: additive 2 · warn + measure warnings[] on omission adoption tracked per key 3 · enforce 422 naming the field schema marks it required diff: breaking, approved

Step-by-step

Step 1 — Ship it optional, with the default in one place

// create-order.ts — the default and the deadline live together
const CHANNEL_DEFAULT = "web";
const CHANNEL_REQUIRED_FROM = Date.parse("2026-11-30T00:00:00Z");

export async function createOrder(req: Request, res: Response) {
  const suppliedChannel = req.body.channel as string | undefined;
  const enforcing = Date.now() >= CHANNEL_REQUIRED_FROM;

  if (!suppliedChannel && enforcing) {
    return res.status(422).type("application/problem+json").json({
      type: "urn:api:errors:v1:validation-failed",
      title: "Request validation failed",
      status: 422,
      violations: [{
        pointer: "/channel",
        code: "required",
        message: "channel became required on 2026-11-30.",
      }],
    });
  }

  const channel = suppliedChannel ?? CHANNEL_DEFAULT;
  const order = await orders.create({ ...req.body, channel });

  const body: Record<string, unknown> = { data: order };
  if (!suppliedChannel) {
    body.warnings = [{
      code: "channel_will_become_required",
      message: `channel was defaulted to "${CHANNEL_DEFAULT}". It becomes required on 2026-11-30.`,
    }];
    res.setHeader("Deprecation", "true");
    res.setHeader("Sunset", "Mon, 30 Nov 2026 00:00:00 GMT");
  }
  res.status(201).json(body);
}

Emitting Sunset on the request pattern rather than the endpoint is a slight stretch of RFC 8594 — the header describes a resource’s retirement — but it puts the date somewhere a generic client already looks. Pair it with the in-band warning so the signal reaches both machine and human readers, following the same in-band principle as Sunset & Deprecation Headers.

Step 2 — Measure adoption per caller

-- Who still omits the field, and when did they last do it?
SELECT api_key_id,
       count(*) FILTER (WHERE channel_supplied)       AS with_channel,
       count(*) FILTER (WHERE NOT channel_supplied)   AS without_channel,
       max(occurred_at) FILTER (WHERE NOT channel_supplied) AS last_omission
FROM   order_creation_log
WHERE  occurred_at > now() - interval '14 days'
GROUP  BY api_key_id
HAVING count(*) FILTER (WHERE NOT channel_supplied) > 0
ORDER  BY without_channel DESC;

This query is the difference between “we announced it, so it is their problem” and “four integrations are affected, and here is who to email”. It also tells you when adoption has plateaued, which is the real signal that the remaining callers will never move without enforcement.

Step 3 — Decide the date from the data, not a policy

Setting the enforcement date from observed adoption A curve rises from near zero to about ninety-eight percent over several weeks and then flattens. The enforcement date is placed after the plateau, with the remaining callers contacted directly. weeks after the field shipped as optional %supplyingit 100 0 enforcement date plateau at ~98% remaining 2% contacted by name from access logs

Step 4 — Enforce, and let the diff record it

At the enforcement date the schema changes and the spec diff will correctly report a breaking change. That is the point: it should be recorded, approved and released as a major SDK version rather than slipping through.

# .oasdiff-ignore.yaml — the approved, dated break
- id: request-property-became-required
  path: /orders
  method: POST
  reason: >
    `channel` became required on 2026-11-30 as announced 2026-08-15. Adoption
    reached 99.4% and the four remaining callers were contacted individually
    (ticket API-3104).
  approved_by: orders-team
  expires: 2027-01-31

Each stage of the migration produces a different diff classification, which is how the gate stays honest throughout:

How the gate classifies each stage of the migration Adding the optional field is additive, the warning stage changes nothing structural, and the enforcement stage is correctly reported as breaking and approved with a dated suppression. the enforcement stage should be flagged — that is the point optional + default diff: additive ships immediately warn + measure diff: unchanged adoption tracked enforce diff: BREAKING approved, datedsuppression release SDK major bump derived, not chosen a staged break that never appears in the diff means the schema was never updated

Standard compliance

Element Standard Clause Note
required keyword JSON Schema Validation 2020-12 §6.5.3 Applies to the whole instance
default annotation JSON Schema Validation 2020-12 §9.2 Annotation only — never applied by a validator
422 for a missing field RFC 9110 §15.5.21 The body parsed but broke a rule
Deprecation / Sunset RFC 8594 §2, §3 Announcing the enforcement date in-band
Warning conveyance No current standard header; use the body

The default row is the one implementers get wrong. A default in JSON Schema is documentation, not behaviour: no validator fills it in. The server must apply the default explicitly in code, and the schema annotation exists so consumers and generated clients know what will happen when they omit the field.

SDK / codegen downstream effect

  // Stage 1 — generated client, field optional
  export interface OrderCreate {
    items: OrderItem[];
-   channel?: "web" | "mobile" | "partner" | "internal";
+ }
+ // Stage 3 — generated client after enforcement (major version)
+ export interface OrderCreate {
+   items: OrderItem[];
+   channel: "web" | "mobile" | "partner" | "internal";
  }

At stage 1 the SDK compiles for callers who omit the field, so upgrading is safe and they can adopt at leisure. At stage 3 the property becomes non-optional and their code stops compiling — which is exactly the desired behaviour for a major version, because a compile error at upgrade time is far better than a 422 in production. Publishing that major release follows Publishing Generated SDKs from GitHub Actions.

Common mistakes

Mistake Correct approach
Marking the field required in the same release it appears Ship optional with a default first
Relying on JSON Schema default to populate the value Apply the default in server code; the annotation is documentation
Announcing only in a changelog Warn in-band on every affected response
Picking the enforcement date from a policy Derive it from measured adoption
Enforcing while adoption is still climbing Wait for the plateau, then contact the remainder
Inventing a default that is wrong for some callers If no honest default exists, version instead
Enforcing without updating the schema Spec and behaviour must change together

FAQ

Can a default value make a new required field non-breaking?

It makes the transition non-breaking, not the requirement. While the server substitutes a default, the field is optional in practice: existing clients keep working unchanged, and the spec diff correctly classifies the addition as additive. What a default cannot do is make the eventual enforcement free — the day the server starts rejecting omissions, every client that never adopted the field breaks. Think of the default as buying a migration window rather than avoiding the migration, and put the enforcement date in the field description from day one so nobody mistakes the window for permanence.

What if no safe default exists?

Then the field genuinely cannot be added to the existing operation, and pretending otherwise is the worst option available. A default that is wrong for some callers converts a loud, immediate failure into silent bad data that surfaces weeks later in reconciliation. The honest choices are a new operation or resource variant that requires the field from the start, leaving the old one deprecated, or a new API version. Both are more work than a default, and both are far less work than correcting three months of mis-attributed records.

How long should the transition window be?

Long enough for your slowest consumer to notice, plan, implement and release — which you should measure rather than guess. For internal services deployed weekly, two to four weeks is often enough. For third-party integrations, quarterly release cycles and change-control processes make three to six months realistic. For embedded or on-premises clients, a year is not unusual. The reliable signal is the adoption curve: when it plateaus, remaining callers are not moving on their own, and the right response is direct contact plus a firm date — not another month of waiting.