DEV Community

Morgan Sun
Morgan Sun

Posted on

The HTTP Fixture Is Canonical. SDK Samples Are Syntax.

A review queue once held two snippets from the same docs page. The Python sample put customer_id in the query string. The curl sample put it in the JSON body. Both had been drafted the same afternoon, from the same OpenAPI file, by the same class of model.

This is a reconstructed incident pattern, not a customer report. It is common enough that the failure is structural. Multi-language examples are not “the same request in different syntax” unless something outside the model owns the request.

The model is a reasonable syntax clerk. It is a poor registrar of HTTP identity. Docs that let it own both jobs will drift, and the drift is hard to see in a markdown diff.

What actually diverged

The OpenAPI document still said POST /v1/invoices with customer_id in the body. Neither sample was “unauthorized.” Each was locally plausible. Python HTTP clients often encourage query params. Curl examples often start from a JSON blob the model saw in a tutorial.

Three classes of drift show up in review:

  • Location drift: a field moves between path, query, header, and body.
  • Shape drift: a scalar becomes an object, a string becomes an integer, an optional field becomes required in the sample only.
  • Header drift: Authorization vs X-Api-Key, Content-Type omitted, Idempotency-Key invented or dropped.

None of these are spelling problems. They are contract problems wearing syntax.

Ownership split

Treat every SDK sample as two documents glued together. One of them may be drafted. The other must be signed.

The model may draft

  • Import lines and client-library boilerplate
  • Local variable names, comments, and language idioms
  • How the frozen request is expressed in that HTTP library
  • Retry/sleep examples that are clearly marked non-normative

A human must own

  • Method, path, and which parameters live where
  • Auth scheme and which headers are required
  • Field names, types, and requiredness for the example payload
  • Status codes presented as contractual
  • Idempotency, pagination, and ordering promises
  • Any numeral that looks like a quota, SLA, timeout, or rate limit

If a sentence would still be true after you deleted every SDK, a human owns it. If it would be false after you swapped Python for Go, the model may draft it.

Freeze one fixture per operation

Do not start from prose. Start from a file that cannot speak in two dialects at once.

# fixtures/create_invoice.yaml
id: create_invoice
method: POST
path: /v1/invoices
headers:
  Authorization: "Bearer $API_KEY"
  Content-Type: application/json
  Idempotency-Key: "$IDEMPOTENCY_KEY"
query: {}
body:
  customer_id: cus_123
  currency: usd
  amount_cents: 1999
ownership:
  human:
    - method
    - path
    - headers.Authorization
    - headers.Idempotency-Key
    - body
  model:
    - language_syntax
    - comments
    - client_library_calls
Enter fullscreen mode Exit fullscreen mode

The fixture is the only object that may define the wire request. Examples may restate it. They may not edit it.

Each sample then carries a machine-readable echo, not as documentation for humans, but as a checksum for CI:

# examples/create_invoice.py
# HTTP-CANONICAL: {"id":"create_invoice","method":"POST","path":"/v1/invoices","headers":{"Authorization":"Bearer $API_KEY","Content-Type":"application/json","Idempotency-Key":"$IDEMPOTENCY_KEY"},"query":{},"body":{"customer_id":"cus_123","currency":"usd","amount_cents":1999}}

import os
import requests

resp = requests.post(
    "https://api.example.com/v1/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": os.environ["IDEMPOTENCY_KEY"],
    },
    json={
        "customer_id": "cus_123",
        "currency": "usd",
        "amount_cents": 1999,
    },
    timeout=30,
)
print(resp.status_code)
Enter fullscreen mode Exit fullscreen mode

The timeout=30 is a client default in the sample. It is not an SLA. Keep that distinction boring and explicit. If the page needs a server-side time guarantee, a human writes it next to a source, not inside the SDK snippet.

A checker you can run

The script below is a proposal you can save as tools/check_example_fixtures.py. It uses only the standard library. It does not call a network. It fails when the canonical block disagrees with the fixture, or when the surrounding markdown makes a guarantee the fixture does not support.

#!/usr/bin/env python3
"""Fail CI when SDK samples drift from frozen HTTP fixtures."""
from __future__ import annotations

import json
import pathlib
import re
import sys

ROOT = pathlib.Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "fixtures"
EXAMPLES = ROOT / "examples"
CANON_RE = re.compile(r"HTTP-CANONICAL:\s*(\{.*\})")
GUARANTEE_RE = re.compile(
    r"\b(always|never|guarantees?|SLA|p99|rate limit|timeout of)\b",
    re.I,
)

def load_fixtures() -> dict[str, dict]:
    out = {}
    for path in sorted(FIXTURES.glob("*.yaml")):
        # Tiny YAML subset: "key: value" and nested 2-space maps.
        data = parse_simple_yaml(path.read_text())
        out[data["id"]] = data
    return out

def parse_simple_yaml(text: str) -> dict:
    """Enough YAML for this fixture shape. Not a general parser."""
    try:
        import yaml  # optional
        return yaml.safe_load(text)
    except ImportError:
        raise SystemExit("Install pyyaml, or emit fixtures as JSON.")

def canonical_view(fx: dict) -> dict:
    return {
        "id": fx["id"],
        "method": fx["method"],
        "path": fx["path"],
        "headers": fx.get("headers") or {},
        "query": fx.get("query") or {},
        "body": fx.get("body") or {},
    }

def check_example(path: pathlib.Path, fixtures: dict) -> list[str]:
    errors = []
    text = path.read_text()
    match = CANON_RE.search(text)
    if not match:
        return [f"{path}: missing HTTP-CANONICAL block"]
    try:
        echo = json.loads(match.group(1))
    except json.JSONDecodeError as exc:
        return [f"{path}: canonical JSON is invalid ({exc})"]
    fx = fixtures.get(echo.get("id"))
    if fx is None:
        return [f"{path}: unknown fixture id {echo.get('id')!r}"]
    expected = canonical_view(fx)
    if echo != expected:
        errors.append(
            f"{path}: canonical echo drifted from fixtures/{fx['id']}.yaml\n"
            f"  expected: {json.dumps(expected, sort_keys=True)}\n"
            f"  found:    {json.dumps(echo, sort_keys=True)}"
        )
    # Samples may include timeout kwargs. Prose around them may not promise SLA.
    md = path.with_suffix(".md")
    if md.exists():
        prose = md.read_text()
        if GUARANTEE_RE.search(prose):
            errors.append(
                f"{md}: guarantee language is human-owned; "
                f"move it out of the generated page or cite a source"
            )
    return errors

def main() -> int:
    fixtures = load_fixtures()
    errors: list[str] = []
    for path in sorted(EXAMPLES.glob("create_*.*")):
        if path.suffix in {".py", ".js", ".ts", ".go", ".rb", ".java"}:
            errors.extend(check_example(path, fixtures))
    for line in errors:
        print(line, file=sys.stderr)
    print(f"checked {len(list(EXAMPLES.glob('create_*.*')))} examples, "
          f"{len(errors)} error(s)")
    return 1 if errors else 0

if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Run it locally:

pip install pyyaml
python tools/check_example_fixtures.py
Enter fullscreen mode Exit fullscreen mode

A green run means the samples still confess the same request. It does not mean the production API matches the fixture. That is a different test, against a staging server or a recorded contract suite.

Where a model is allowed to work

Generation sits after the fixture is signed. The prompt may see the YAML. It may not propose edits to it. Ask only for one language at a time, and require the HTTP-CANONICAL line to be copied, not rewritten.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you use MonkeyCode's free model access — and, when you want the job off your laptop, the free server option — send the frozen fixture plus a language name. Do not send a blank “write docs for this endpoint” prompt. The output you keep is syntax. The object you merge first is still the YAML.

A prompt that respects the split looks like this:

You are filling SDK syntax for one frozen HTTP fixture.
Copy the HTTP-CANONICAL JSON exactly. Do not add, drop, or relocate fields.
Do not invent rate limits, SLAs, status codes, or auth schemes.
Language: Python 3, stdlib + requests.
Fixture follows:
<paste fixtures/create_invoice.yaml>
Enter fullscreen mode Exit fullscreen mode

If the model returns a different path, the checker fails. That is the point. Reviewers then stop arguing about taste in variable names and start arguing about whether the fixture itself is wrong.

Decision table

Claim in the docs page Draftable by a model? Merge rule
requests.post(...) boilerplate Yes Must echo fixture
Path /v1/invoices No Fixture only
customer_id in body vs query No Fixture only
Idempotency-Key required No Human signs fixture
“Timeout of 30s is guaranteed” No Block; needs a measured source
“This client retries twice” Yes, if labeled client-side Must not imply server behavior
Example cus_123 Yes Keep clearly fake
“We never return 409 after commit” No Human-owned failure contract

Print the table in the docs repo README. Authors stop treating “the model wrote it” as a review status.

What this does not prove

The checker is a string gate. It will not parse Python AST and reconstruct the real requests.post call unless you add that later. A sample can echo the right canonical block and still execute the wrong code. Pair this with at least one compiled or executed example per language when the client library makes that cheap.

It also will not save a wrong fixture. If a human freezes amount_cents as a string, every language sample will be consistently wrong. Fixture review is still engineering review.

YAML here is a convenience. JSON fixtures are easier to diff in some repos. The ownership rule does not care.

Do not use this approach when:

  • There is no canonical HTTP contract, only a GUI walkthrough
  • Examples are intentionally non-normative (blog posts, conference talks)
  • A single language is hand-maintained and never regenerated
  • The “sample” is an SDK that hides the wire format so deeply that an HTTP echo would lie

Those cases need a different artifact: a recorded traffic file, a Pact-style contract, or a human-written tutorial with no generated samples at all.

Keep the regen loop small

A practical sequence:

  1. Human edits fixtures/create_invoice.yaml in the same PR as the API change.
  2. Model drafts or refreshes one sample per language, canonical block included.
  3. python tools/check_example_fixtures.py runs in CI.
  4. Reviewers read the fixture diff first, sample diffs second.
  5. Guarantee sentences stay in a human-owned markdown file that regeneration cannot overwrite.

The cost of skipping step 1 is not a messy paragraph. It is two support snippets that disagree about where customer_id lives. Syntax is cheap to regenerate. Identity is not.

Top comments (0)