Spring til indhold

JSON

tekstformatet, de fleste data rejser i

Hvad det er

JSON er en tekstnotation for strukturerede data. Den skrives med krøllede og firkantede parenteser, dobbelte anførselstegn og kommaer, så et program i stort set et hvilket som helst sprog kan læse præcis det tilbage, som et andet program har skrevet. Forkortelsen står for JavaScript Object Notation, fordi syntaksen er lånt fra JavaScript, men et JSON-dokument tilhører intet bestemt sprog. Filerne ender som regel på .json, og over HTTP bliver indholdet mærket med medietypen application/json.

Hele formatet kan stå på én side. Ordforrådet består af fire simple typer (strenge, tal, værdierne true og false samt null) og to sammensatte: et objekt, som er en uordnet samling af navne og værdier, og et array, som er en ordnet liste. Alt kan indlejres i alt andet. Datoer, binære data, kommentarer og henvisninger mellem dele af et dokument klares med konventioner uden for formatet.

Douglas Crockford registrerede første gang formatet hos IETF i RFC 4627 i juli 2006. Den gældende definition er RFC 8259 fra december 2017. Den er enig med Ecmas standard ECMA-404 om grammatikken og tilføjer de regler, der tæller, når to systemer udveksler data: tekst, der forlader et lukket økosystem, skal være UTF-8, og afsenderen må ikke sætte et byte order mark foran.

XML er ældre og mere udtryksfuldt med attributter, navnerum og en lang tradition for skemasprog. YAML blev i version 1.2 gjort til et strengt supersæt af JSON og lægger de kommentarer og den lettere tegnsætning oveni, som folk vil have i filer, de redigerer i hånden. Binære formater som Protocol Buffers og CBOR bytter læsbarhed for størrelse og hastighed.

Hvorfor det betyder noget

Den, der vælger JSON, vælger i virkeligheden en kontrakt mellem to systemer: formen på det, der sendes over nettet, og nogle gange formen på det, der ender i databasen.

Over nettet er JSON standardvalget, fordi alt allerede taler det. En browser læser det uden et bibliotek, alle udbredte sprog har en moden parser, og en udvikler, der læser en log, kan se, hvad en forespørgsel indeholdt. Omkostningerne ligger et andet sted. Hver post gentager sine feltnavne, tal rejser som decimaltekst, der skal parses tilbage, og formatet siger ikke selv noget om, hvilke felter der er obligatoriske, eller hvilken type de har. Et binært, skemabaseret format som Protocol Buffers er mindre og hurtigere at parse, og en compiler tjekker kontrakten, men et menneske skal bruge et værktøj for at læse en enkelt besked. Til et offentligt API eller et, som browseren taler med, er JSON det fornuftige valg. Mellem interne services, hvor volumen eller latens vejer tungest, kan regnestykket falde ud til den anden side.

Et skema leverer typisk den kontrakt, der mangler. JSON Schema beskriver, hvilke egenskaber et objekt skal have, hvilke typer de har, og om ekstra egenskaber er tilladt, og OpenAPI bygger videre på det for at beskrive et helt HTTP-API. Skemaet er et separat dokument og passer kun, så længe noget tvinger det til at følge koden.

I databasen er spørgsmålet, om en struktur hører hjemme i en JSON-kolonne eller i rigtige kolonner. PostgreSQL har to typer. json gemmer en nøjagtig kopi af inputteksten og parser den igen, hver gang en funktion rører den, mens jsonb gemmer en nedbrudt binær form, der er lidt langsommere at skrive, langt hurtigere at søge i og kan indekseres. Dokumentationen anbefaler jsonb til de fleste applikationer. En JSON-kolonne passer til data, hvis form varierer fra række til række eller tilhører klienten, for eksempel en brugers gemte indstillinger. Et felt, som databasen skal joine på, håndhæve en begrænsning på eller summere, fortjener sin egen kolonne.

Sådan ser det ud i praksis

På én platform bygger PostgreSQL-funktionerne selv hele svaret som JSON, og Go-API’et sender den JSON uændret videre fra databasen til browseren. Handleren modtager svaret som json.RawMessage og giver det videre uden at afkode eller genkode det, og hjælpefunktionen QueryJSON gjorde det til standarden. De DTO’er, mappere og svarstrukturer, et lagdelt API plejer at samle, forsvandt. Ændrer en funktion sit output, når den nye form helt frem til klienten uden en eneste rettelse i handlerkoden.

Gemt JSON giver et andet problem, fordi den lever længere end den kode, der skrev den. En platform gemmer notifikationsindstillinger og lignende præferencer som brugerens egne værdier lagt oven på et sæt standardværdier, oprindeligt flettet i ét niveau. En nøgle, der senere kom til i standardværdierne, manglede i alle rækker gemt før den, så klientkoden læste et felt som undefined og gik ned, og det ramte netop de brugere, der havde været med længst. Løsningen blev en deep merge, der bruger standardværdierne som katalog: hver nøgle i kataloget kommer ud, også de indlejrede, uanset hvad den gemte klump indeholder. En migrering førte det ind i de eksisterende rækker med det samme, og frontenden kunne droppe sine defensive tjek.

Data ankommer også ofte som JSON. Til historiske elektricitetsdata leveret som JSONL, én JSON-hændelse pr. linje, gik opgaven ud på at gøre rå hændelser til timeforbrug pr. zone, og der blev bygget tre varianter, i Python, JQ og SQL. Python var hurtigst med 34 ms for én dags data og 180 ms for 30 dage.

Hvor det går galt

Grammatikken er så lille, at parsere er enige om, hvad gyldig tekst er. Problemerne opstår i alt det, grammatikken lader stå åbent.

Et stort id kommer tilbage med et andet ciffer. Grammatikken sætter intet loft over tal, men de fleste parsere, også JavaScripts egen, læser dem ind som 64-bit flydende kommatal, der kun rummer heltal præcist op til 2⁵³ − 1. RFC 8259 udpeger intervallet fra −(2⁵³ − 1) til 2⁵³ − 1 som det, implementeringer er enige om. Over den grænse ændrer id’er sig uden varsel:

JSON.parse('{"id": 9007199254740993}').id;
// 9007199254740992

Send 64-bit-id’er som strenge, og send beløb som strenge eller som et heltal af den mindste møntenhed, aldrig som kommatal.

To services læser det samme objekt forskelligt. RFC 8259 siger, at navnene i et objekt bør være unikke, og lader resten stå åbent: møder en parser et gentaget navn, beholder mange den sidste værdi, andre melder fejl, og nogle returnerer alle parrene. Når en gateway tjekker den første role, og backenden handler på den anden, har tjekket beskyttet ingenting. Afvis dublerede navne der, hvor data kommer ind i systemet. I-JSON, defineret i RFC 7493, er en profil af JSON, der forbyder dem.

En håndredigeret fil holder op med at parse. Kommentarer og afsluttende kommaer ligger uden for grammatikken, så den første, der skriver en kommentar i en konfigurationsfil eller efterlader et komma efter sidste element, ødelægger den. Visse værktøjer accepterer en løsere dialekt, og så er den samme fil gyldig for én læser og ugyldig for den næste. Brug JSON til data, som maskiner skriver, og giv filer, folk vedligeholder i hånden, et format lavet til redigering, for eksempel YAML eller TOML.

Teksten kommer forvansket frem, eller én parser afviser, hvad en anden accepterer. RFC 8259 kræver UTF-8 mellem systemer og forbyder et byte order mark i starten. Grammatikken tillader også en escape-sekvens som "\uDEAD", den ene halvdel af et surrogatpar uden sin partner, som ingen gyldig Unicode-streng kan rumme, og bibliotekerne håndterer den hver på sin måde. Kod som UTF-8 ved kilden, og afvis ugyldige sekvenser ved grænsen.

En lille forespørgsel binder parseren. Nogle få kilobyte indlejrede klammer kan sende en rekursiv parser tusindvis af niveauer ned, og en overstor body bliver parset helt, før noget skema ser den. RFC 8259 tillader en implementering at begrænse både tekstens størrelse og indlejringsdybden, og et API, der tager imod JSON fra fremmede, bør bruge begge dele: et loft over størrelsen, før der læses, et loft over dybden under parsningen og først derefter et tjek af formen.

Læs mere

Ordliste