Wednesday, June 3, 2026

Stop Breaking Your API with Primitive Drift: A Guide to Semantic Type Modeling

If you’ve ever debugged a weird rounding error in a financial transaction or watched a perfectly good API fail because a description field suddenly allowed emojis, you’ve already met the problem this article solves.

It’s called Primitive Drift, and it’s quietly destroying your API reliability.

Let me show you how to fix it using Semantic Type Modeling —and why your E2E tests are probably making things worse.

The Problem: When Strings and Numbers Become Liabilities

Most API designs start simple. You map a business concept directly to a JSON primitive:

description:
  type: string
price:
  type: number

Seems harmless. But over time, this causes three predictable disasters:

  1. Inconsistent rules – One team limits descriptions to 250 characters. Another allows 1000. Good luck explaining that to customers.
  2. Financial fraud, literally – Floating-point numbers can’t represent cents exactly. 0.01 + 0.02 isn’t always 0.03. Attackers exploit this for fractional-cent theft.
  3. Leaky security – Each microservice reinvents XSS or SQL injection validation. Gaps appear. Compliance fails.

The solution isn’t more E2E tests. It’s a single source of truth for what your data actually means.

Semantic Type Modeling: Treat Data Like a Business, Not a Compiler

Instead of seeing string, see ItemDescription. Instead of number, see Money.

You define the rule once:

ItemDescription:
  type: string
  minLength: 10
  maxLength: 500
  pattern: '^[a-zA-Z0-9\s.,!?()"-]+$'

Then everywhere else, you just reference it:

description:
  $ref: './domain-types.yaml#/components/schemas/ItemDescription'

That’s it. One change propagates everywhere. No drift. No debate.

The Architecture: How It Stays Enforced (Without Nagging)

You don’t need a committee to enforce this. You need automation.

  • Central Domain Library – A single YAML file (domain-types.yaml) holding every canonical business type.
  • API specs – Each endpoint references types from that library using $ref.
  • CI/CD linter – Tools like Spectral block any PR that uses a raw primitive where a domain type exists.
  • API Gateway – Rejects malformed payloads at the edge, before they touch your backend.

Result: Developers get instant feedback, not a broken pipeline hours later.

Why E2E Tests Are the Wrong Hammer for This Nail

I see teams fall into the E2E Test-Driven Specification anti-pattern all the time:

“We don’t know what the API expects, so we’ll throw payloads at staging, see what breaks, and write a test to lock in that behavior.”

That’s expensive, slow, and fragile.

Bug-Driven LoopSemantic Modeling Loop
Write code → Deploy → Run slow E2E → Fail → Fix → Re-deployWrite schema → Linter flags error instantly → Fix → Done
Hours to daysSeconds

If a business rule changes (e.g., SKU format lengthens), bug-driven teams rewrite dozens of E2E tests. Semantic teams change one line in domain-types.yaml.

Shift Left: Make Validation an IDE Feature, Not a CI Surprise

Give developers local mock servers (like Prism) and OpenAPI validators. When they send a bad payload during development, they get an immediate 400 Bad Request with a precise error:

body/description must match pattern ^[a-zA-Z0-9\s.,!?()"-]+$

No waiting. No guessing. No staging environment required.

When E2E Tests Are Still Useful (And When They Aren’t)

Educate your team on the testing pyramid:

  • Contract validation → OpenAPI + linter (zero cost)
  • Integration/unit tests → business logic like discount calculation (low cost)
  • E2E tests → user journeys, multi-system flows (high cost)

If your E2E test fails because a field was an integer instead of a string, delete that test. That constraint should have been caught before the code was compiled.

But What About Cross-Field Logic? (“If A then B required”)

Developers often argue: “Sure, single-field validation works, but what about conditional rules? That needs an E2E test.”

That argument is outdated. Modern JSON Schema (OpenAPI 3.x) supports oneOf, allOf, and conditional logic natively.

Example: If payment method is CREDIT_CARD, require card details. If PAYPAL, require a redirect URL.

OrderPayload:
  type: object
  required: [orderId, paymentMethod]
  properties:
    orderId: {type: string}
    paymentMethod: {type: string, enum: [CREDIT_CARD, PAYPAL]}
  allOf:
    - if:
        properties: {paymentMethod: {const: CREDIT_CARD}}
      then:
        required: [cardDetails]
    - if:
        properties: {paymentMethod: {const: PAYPAL}}
      then:
        required: [paypalRedirectUrl]

No E2E test needed. The schema enforces it directly.

How to Roll This Out Without Causing a Rebellion

Phase 1: Audit
Look at your existing E2E suite. Flag every test that checks a simple field constraint (description too long, price negative).

Phase 2: Migrate
Move those constraints into domain-types.yaml. Add Spectral to your CI pipeline.

Phase 3: Delete
Remove those E2E tests. Watch build times drop.

Phase 4: Enforce
Make updating the OpenAPI schema a prerequisite for starting development on any feature that touches data shapes.

The Bottom Line

Semantic Type Modeling isn’t just cleaner — it’s faster, safer, and cheaper.

  • API gateways reject attacks at the perimeter
  • SDKs auto-generate correct validation code
  • Documentation stays accurate without manual updates

Stop reverse-engineering your own API. Start modeling what your business actually means.

Your future self (and your on-call rotation) will thank you.


Want the full OpenAPI examples and Spectral rules? Check out the companion repo [link].

No comments:

Post a Comment