Cursor vs offset pagination schema design
Symptom: CI pipelines fail with ValidationError: strict mode: additional properties not allowed ("next_cursor") or Missing required property: "offset" during OpenAPI or Pact contract execution. Frontend builds break with TS2339: Property 'offset' does not exist on type 'CursorPaginationMeta'. Automated snapshot diffs in staging reveal inconsistent meta.pagination payloads across service boundaries, triggering false-positive regression alerts.
Root Cause Analysis
The API contract enforces a monolithic pagination envelope that assumes a single navigation strategy. Migrating from offset to cursor-based pagination introduces mutually exclusive fields (next_cursor, prev_cursor vs offset, limit). Strict JSON Schema validators and runtime parsers reject the updated envelope because the base definition lacks conditional branching. This violates foundational Schema Design & Validation Patterns by treating pagination metadata as a static object rather than a polymorphic union. Without an explicit discriminator, validators flag valid cursor payloads as structural violations, causing type narrowing failures in generated clients and breaking downstream type generation.
Step-by-Step Resolution
-
Refactor the OpenAPI
PaginationMetacomponent. Replace the flat object definition with aoneOfconditional union that explicitly separates cursor and offset contracts. Introduce a stringtypediscriminator to enable deterministic payload routing and eliminate ambiguous property collisions. -
Apply the exact OpenAPI YAML patch to your specification file:
PaginationMeta:
type: object
discriminator:
propertyName: type
oneOf:
- $ref: '#/components/schemas/CursorPaginationMeta'
- $ref: '#/components/schemas/OffsetPaginationMeta'
CursorPaginationMeta:
type: object
required: [type, next_cursor, prev_cursor]
properties:
type:
type: string
enum: [cursor]
next_cursor:
type: string
nullable: true
prev_cursor:
type: string
nullable: true
OffsetPaginationMeta:
type: object
required: [type, offset, limit, total]
properties:
type:
type: string
enum: [offset]
offset:
type: integer
minimum: 0
limit:
type: integer
minimum: 1
total:
type: integer
- Update runtime validation (Zod) to mirror the discriminator pattern for type-safe parsing:
const cursorMeta = z.object({
type: z.literal('cursor'),
next_cursor: z.string().nullable(),
prev_cursor: z.string().nullable()
});
const offsetMeta = z.object({
type: z.literal('offset'),
offset: z.number().int().min(0),
limit: z.number().int().min(1),
total: z.number().int()
});
export const PaginationMetaSchema = z.discriminatedUnion('type', [cursorMeta, offsetMeta]);
- Regenerate client SDKs using
openapi-generatorand execute the contract test suite. Verify that both cursor and offset payloads resolve correctly without fallback type coercion or union narrowing errors. Confirm thatTS2339and strict-mode validation failures are eliminated across all integration points.
Prevention & Contract Governance
Integrate Spectral linting rules into CI to automatically flag flat pagination objects lacking discriminators. Align all new endpoint contracts with documented Pagination and Filtering Schema Patterns to prevent ad-hoc field additions and schema drift. Mandate polymorphic envelope testing in PR checklists, requiring both cursor and offset payloads to pass strict contract validation before merge. Implement automated schema snapshotting in CI pipelines to detect silent structural changes in pagination metadata across microservice boundaries.