A Field Was Renamed and Only the Total Went Quietly Wrong
Goal
You put 1.3 and 1.4 of the partner order API side by side and decide at the field level what breaks us, then build a consumer contract listing only what we read and a contract test that checks that contract every time. You also attach a reading layer that accepts the old and new versions at the same time.
Why it matters
An incident where the connection drops sets off an alert. An incident where a field changes its name does not. rec.get("region") returns None instead of raising an exception, an added enumerated value enters none of our branches, and when an integer becomes a decimal string, the total quietly changes.
So the safety device in integration is not "reading the documentation well" but a contract that a machine checks every time. In the contract you write not the partner's whole schema but only the fields we actually read. If you write everything, the light turns red even for changes to fields we do not use, and when the noise grows, people turn the tests off.
An upgrade also does not finish all at once. While the old and new versions run together for months, the reading side must accept both, so you gather the places where names changed into an alias table and the places where types changed into a normalization function, in one place.
The grader does not believe your sentences. It starts the partner server you built directly on a port the grader chooses, receives the responses, and reruns your decider and comparator with inputs the grader creates to check the answers.
Steps
- Create /root/contract/partner.py, run it on port 8011, and save the responses of the two versions to /root/contract/v13.json and /root/contract/v14.json.
- Write the field differences between the two responses in /root/contract/diff.json in four slots: added, removed, type_changed, and enum_added.
- Create /root/contract/breaking.py so that it takes one change and decides whether it breaks us.
- Write a consumer contract listing only the fields we read in /root/contract/order.contract.json.
- Create /root/contract/validate.py so that it compares the contract with records and writes violations split into missing, type, and enum.
- Create /root/contract/read_order.py so that it moves both 1.3 and 1.4 responses into the same internal shape.
- Create /root/contract/contract_test.sh so that it compares the partner's current response with the contract and ends with a non-zero code if they differ.
- Report in four sections in /root/contract/contract_report.md.
Notes
- Partner server run contract:
python3 /root/contract/partner.py --port <포트> [--drift](the placeholder is the port)./healthreturns{"ok": true, "versions": ["1.3", "1.4"]}, and/v1.3/ordersand/v1.4/ordersreturn{"version": ..., "orders": [...]}. There are 24 orders. - The differences between the two versions are these. 1.3 returns
order_id,amount(integer),currency,status,region, andupdated_at, and 1.4 returnsorder_id,amount(decimal string),currency,status,market,channel, andupdated_at. In 1.4 thestatushas one more value,on_hold. --driftis the version where the partner has moved again without notice. The step 7 contract test must catch this with a non-zero code.- Decider run contract:
python3 breaking.py --change <파일>(the placeholder is the file) outputs{"breaking": true|false, "reason": "..."}. The change JSON is{"where": "response"|"request", "kind": "...", "field": "..."}, and kind is one of nine: add_field, remove_field, rename_field, type_change, add_enum_value, field_becomes_optional, add_optional_field, add_required_field, and relax_required. The same kind name can appear on both the response side and the request side, and then the answers differ. If a kind not in the table arrives, answer that it breaks, not that it is safe. - Comparator run contract:
python3 validate.py --contract <파일> --records <파일>(the placeholders are the files) outputs{"records": n, "ok": n, "violations": [{"index": i, "field": f, "kind": k}]}. kind is missing, type, or enum. The type names are the three string, integer, and decimal_string.okis the number of records with no violation at all. - Reading layer run contract:
python3 read_order.py --in <응답 파일>(the placeholder is the response file) outputs a list of normalized records. Each record has five slots:order_id,amount_krw(integer),currency,status, andmarket. - Contract test run contract:
bash contract_test.sh <BASE_URL>ends with 0 if there are no violations and 1 if there are. - The decision rule for this lab: on the response side, only adding a field is safe and the rest is treated as breaking. On the request side, only adding a required field breaks. This is a consumer-side rule and not something the RFCs define.
- Common mistakes: writing all of the partner's fields in the contract (the light turns red even for changes we do not use), normalizing amounts as float (rounding appears), and running the partner server in the foreground so the terminal is blocked.
- Run the server in the background, wait until
curl -sf http://127.0.0.1:8011/healthworks, and then move on. The grader does not look at the process you left running but restarts the scripts directly.
Start a partner that serves both versions
Create /root/contract/partner.py, run it on port 8011, and save the responses of /v1.3/orders and /v1.4/orders to /root/contract/v13.json and /root/contract/v14.json respectively. There are 24 orders in both versions.
Make three routes with flask. /health is the place that signals readiness, and the two versions' list routes return the same 24 orders, each with its own field names and types. Take --port and --drift with argparse. If you start it in the foreground the terminal is blocked, so start it in the background and wait until /health is 200.
Extract the difference between the two versions in machine-readable form
In /root/contract/diff.json, write four slots: added, removed, type_changed, and enum_added. added and removed are sorted lists of field names, type_changed is a list of {"field": ..., "from": ..., "to": ...} (the type names are integer and string), and enum_added is a list of {"field": ..., "values": [...]}. Only fields with 6 or fewer distinct values in the new version are treated as enumerations.
Looking at only the first record of the two response files gives you the set of field names, but enumerated values show only if you scan everything. Two type names, integer and string, are enough. For a field with only a few kinds of values such as status, subtract the two sides' value sets.
Decide what breaks us
Create /root/contract/breaking.py so that it decides one change received with --change <파일> (the placeholder is the file) and outputs {"breaking": true|false, "reason": "..."}. The rules for the response side and the request side differ.
The response is the side we read, so only additions are safe. The request is the side we send, so the direction is reversed — an optional field being added or a required field being relaxed to optional is nothing to us. A single table keyed on the (where, kind) pair is enough, and for an unknown pair, answer that it breaks, not that it is safe.
A contract listing only what we read
Write the consumer contract in /root/contract/order.contract.json. version is 1.4, unknown_fields is ignore, and in fields list only the five fields we actually read (order_id, amount, currency, status, market), each with type and required. Add an enum to currency, status, and market too.
You will want to write every field the partner provides, but if you list the fields we do not read (channel, updated_at), our test turns red every time they change those fields. The type names are the three string, integer, and decimal_string, and the amount in 1.4 is a decimal string. Get status's enumerated values by scanning the whole 1.4 response.
Check the contract against the actual response
Create /root/contract/validate.py so that it takes --contract and --records and outputs {"records": n, "ok": n, "violations": [...]}. Split violations into three, missing, type, and enum, and attach index and field to each violation.
A single record can have two violations, so violations can be several lines per record. On the other hand, ok is the number of records with no violation at all, so it does not come from subtracting the number of violation lines. A missing field that is not required is not a violation, and do not check the enumeration again on a value whose type is already wrong.
Accept the old and new versions together
Create /root/contract/read_order.py so that it reads both 1.3 and 1.4 responses with --in <응답 파일> (the placeholder is the response file) and moves them into the same internal shape. Each record has five slots: order_id, amount_krw (integer), currency, status, and market, and unknown fields are dropped.
Gather the places where names changed into one alias table and the places where types changed into one normalization function. If you scatter ifs around, you will not know where to delete when you cut off the old version. Do not go through float for the amount — if you cut "12300.00" at the dot and convert it to an integer, no rounding appears.
Catch a partner that moved again without notice
Create /root/contract/contract_test.sh so that, with bash contract_test.sh <BASE_URL>, it compares the partner's current /v1.4/orders with the contract. It must end with 0 if there are no violations and 1 if there are, and when there are violations, the number must remain on the screen.
Use the validate.py and order.contract.json you built before as they are. All that is new is the part that fetches the response and the exit code. Run it against a partner started with --drift and confirm it gives 1, and against a normal partner and confirm it gives 0.
Version upgrade inspection report
In /root/contract/contract_report.md, write four sections, ## 무엇이 바뀌었나, ## 무엇이 우리를 깨뜨리나, ## 우리가 지킬 계약, and ## 다음부터 어떻게 잡나 (in order: what changed, what breaks us, the contract we keep, how we catch it from now on). The field names and violation counts you obtained in the previous steps must be in the body.
The reader may be not our team lead but someone at the partner company. Write not "it broke" but "which field changed how, and what on our side went wrong." Leave the four section titles as they are and fill in the numbers with the values you obtained.