Blog Data formats No. 06
Designing a JSON schema for purchase order data
When we designed the output of VioPO, the aim was a format a program could rely on without defensive code around every field. This article explains the choices, because they apply to any purchase order format, whether you use ours or build your own.
Every key, every time
The most useful property of a format is that it does not change shape. In VioPO's output every key is present in every response. A value that is not on the document is null; an empty list is [].
The alternative, leaving keys out when there is no value, pushes a check for existence into every line of client code, and makes "not on the document" look the same as "a bug dropped it".
Dates as dates
Purchase orders print dates every way there is: 14/08/2026, 08/14/2026, 14 August 2026, Aug 14, 26. The format should not. We return YYYY-MM-DD and nothing else.
The hard case is a numeric date where both parts are 12 or less, such as 03/04/2026. We decide once per document. If any date on it has a first part over 12, the document is day first; if any has a second part over 12, it is month first. Otherwise US documents are month first and everything else day first. A date that cannot be read is left null and named in warnings.
Amounts as numbers
£1,357.85, 1.357,85 EUR and GBP 1357.85 are the same amount. We return 1357.85 and give the currency once, in the header, as an ISO 4217 code. Mixing a currency symbol into an amount string is how sums go wrong later.
JSON numbers are floating point, which can matter for money in some languages. If you add many amounts, convert to integer pence or a decimal type on your side.
Addresses split, but not too far
An address is name, lines, city, region, postal_code and country. Street lines stay as a list in printed order, because trying to split "Unit 4, Canal Side Works" into building and street is more likely to break it than help. Town and postcode come out of the lines, so you do not get the postcode twice.
country is an ISO 3166 code. When no country is printed we infer it only from a postcode shape that leaves no doubt, a UK postcode or a US state with a ZIP code.
Units as codes
Units are where free text hurts most: EA, ea., each, Each, unit, pcs. We map common units to UN/ECE Recommendation 20 codes, the list used in UBL and PEPPOL, so each becomes EA and box becomes BX. A unit we do not recognise is passed through as printed, in upper case, rather than guessed.
Line numbers as positions
Some orders number lines 1, 2, 3; SAP-style orders use 10, 20, 30; some have none. We return line_number as the position from 1. The printed number is rarely needed downstream and the position is always there.
Room for both part numbers
Lines carry buyer_part_number and supplier_part_number separately. An order often has only one, and which one it is matters: the buyer's code needs a cross-reference, yours does not.
Checks in the output
The format includes the result of arithmetic checks and a list of warnings. Putting them in the response, rather than in logs, means the program that receives the order can decide what to do with it.
Versioning
The schema has a name, viopo.po.v1, in every response. Fields can be added within a version; none are removed or change type. A breaking change gets a new version.
The full schema is published at /schema/po.v1.json and described in the docs.