JSONSchema
04.12.22
Introduksjon
Kan vi stole på at sluttbrukerne av tjenestene våre bruker tjenesten slik den er tiltenkt? Erfaringsmessig er svaret dessverre nei. Mangel på kompetanse, misforståelser og snarveier er noen av årsakene til feil bruk av en tjeneste. I starten av karrieren som utvikler hadde jeg gleden av å samarbeide med flere dyktige testere som har vært en kilde til både inspirasjon og frustrasjon. Det har inspirert meg til å luke ut alle kilder til feil og frustrert meg når testerne likevel klarer å finne en ny feil. Det er lett å trekke en assosiasjon til tiden når jeg som ung vernepliktig skulle stille rommet på kasernen klart for inspeksjon til et støvtørst befal. For hver dag som gikk var vi helt sikre på at dette var dagen befalet skulle slå ut med hendene og si noe ala «Her finner jeg ingenting å klage på» eller «Dere har det reneste rommet jeg noen gang har sett» eller «Dette er det reneste rommet i historien». Uansett hvor godt vi vasket så hadde befalet alltid et nytt triks på lur. Da vi endelig hadde mestret lakenstrekken og eliminert hvert eneste støvkorn fra det blå linoleumsgulvet så gikk befalet rett bort til vinduet og dro en hvit hanske over toppen av gardinstangen. Pokker! Enda et sted å passe på å vaske til i morgen. Denne syklusen fortsatte hver dag i flere uker. Hver gang befalet fant støv så ble vi litt bedre til å vaske. I dag, mange år senere er jeg fremdeles sikker på at befalet etter noen måneder smuglet inn støv fra egen lomme for å opprettholde maktforholdet. Den mer sannsynlige teorien er at han ikke behøvde det. Ved å arbeide med gode testere og utålmodige sluttbrukere som ikke oppfører seg slik du ønsker så går man som utvikler igjennom samme syklusen. For hver feil du må rette så danner du en huskeliste over potensielle feil til neste gang. I likhet med rommet på kasernen så blir koden litt renere. Det er kanskje ikke helt dumt å kalle det programvarehygiene.
Tjenester som skal konsumere informasjon eller valg fra en sluttbruker bør vurdere å inkludere validering. Det er en veldig lavterskel metode å luke ut feilsituasjoner og opprettholde god datahygiene. Personlig har jeg arbeidet med mange ulike måter å validere data på. Dette strekker seg fra å benytte Redux til å validere et skjema direkte i nettleseren til å skrive egne valideringsregler på serveren. I flere tilfeller har prosjektet hatt begge deler. Jeg minnes at teknisk ansvarlig kalte det for «belte og bukseseler». Jeg synes det var passende. I år har jeg blitt introdusert for en metode å validere skjemainnhold på som jeg har blitt litt glad i. Det føles underlig å skrive fordi mitt første møte med denne metoden ga meg høye skuldre og mareritt. Eksponeringsterapi var løsningen. I dagens innlegg skal jeg eksponere deg for JSON Schema.
Jeg vet hva JSON er, med hva er et schema?
Kort fortalt er JSON Schema en spesifikasjon. Den har flere versjoner, men i skrivende stund er siste versjon 2020-12. Spesifikasjonen beskriver hvordan man kan konstruere et dokument med regler som vil både definere og validere et JSON-dokument. Dette dokumentet representerer 'schema' i JSON Schema og skal selv bli utformet i JSON. Motivasjonen for prosjektet var å tilby en tilsvarende funksjonalitet som XML Schema til JSON. XML er veldig verbost og mange foretrekker å lese JSON. Det eksisterer flere implementasjoner av JSON Schema. Legg merke til at det er implementasjoner for veldig mange programmeringsspråk. En av fordelene ved å bruke en spesifikasjon som bygger på JSON er at det er teknologi-agnostisk.
Okay, men hvordan fungerer det?
I teksten som følger legges det opp til at leseren kan kjøre eksempler på validering. Før vi begynner med dette bør man ha kjøremiljøet klart. Det første man har behov for er en JSON Schema implementasjon. Dette er selve motoren som utfører kjøringen. Som nevnt i forrige avsnitt så eksisterer det flere implementasjoner og du kan fritt velge en som passer til deg. For eksempel kan du installere NPM-pakken ajv-cli og kjøre valideringen i terminalen. Dersom du kun har lyst til å forsøke eksemplene i denne artikkelen uten å installere noe på maskinen din så vil jeg foreslå å teste i nettleseren din via JSONSchema hyperjump. Det andre vi trenger er et skjema samt litt data vi kan validere.
La oss validere
Et skjema vil også være representert som et JSON-objekt og det enkleste skjemaet vi kan bygge er nemlig kun et tomt objekt {}
. Et tomt objekt inneholder nemlig ingen regler eller informasjon som kan tolkes. Resultatet er derfor at alt er gyldig (gitt at det er gyldig JSON). Dette kan enkelt testes i nettløsningen ved å skrive et tomt objekt ({}
) i skjemafeltet til venstre og teste mot ulike data på høyre side. Forsøk gjerne å validere ugyldig JSON for å se hva som skjer da.
La oss oppdatere skjemaet slik at det faktisk kan gi litt verdi. Legg til feltet type og gi den verdien string.
{
"type": "string"
}
Dette skjemaet vil kun godta en string. Dette kan du verifisere ved å oppdatere testdataene på høyre side i nettleseren. Responsen fra valideringen vil kun være valid dersom det er en string. Alt annet vil være invalid og gi en feilmelding. Type kan være en av følgende:
- string
- number
- integer
- object
- array
- boolean
- null
Metadata og typedefinisjoner
Dersom du har brukt nettløsningen til å validere så har du kanskje lagt merke til at skjemaet kommer preutfylt med feltet $id
og $schema
. Disse to feltene inneholder informasjon om selve skjemaet. Feltet $id
inneholder en URI som kan brukes til å referere til dette skjemaet fra et annet skjema. Feltet $schema
på den andre siden forteller implementasjonen hvilken versjon av spesifikasjonen som skal benyttes. For øyeblikket trenger vi ikke gi disse to feltene mer oppmerksomhet.
Jeg har opprettet et fiktivt ansatt-objekt som inneholder alle ingrediensene man trenger for å demonstrere JSON Schema.
{
"firstname": "Kristoffer",
"middlename": "",
"lastname": "Ronaldo",
"birthday": "25-05-1994",
"salary": 500000,
"contract": true,
"email": "kristoffer.ronaldo@progit.no",
"address": "Madeiragaten 19, 0191 Oslo",
"startDate": "2022-24-11",
"endDate": null,
"skills": [
{
"title": "Java",
"proficiency": "Advanced"
},
{
"title": "JavaScript",
"proficiency": "Advanced"
},
{
"title": "Rust",
"proficiency": "Intermediate"
}
]
}
Dette var litt av en overgang fra det første eksempelet. Vi begynner på starten og tar et steg om gangen. Første steg blir å identifisere at vi har med et objekt å gjøre. Lim inn objektet over i høyre felt og oppdater deretter skjemaet (venstre side) til å se slik ut:
{
"$id": "https://progit.no/employee",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
}
Ansattobjektet vårt vil nå være gyldig. Så lenge det er et objekt så er det gyldig, men dette er ikke god nok validering. La oss oppdatere skjemaet til å forvente visse felter i objektet. En mer detaljert beskrivelse av et objekt skjer innenfor klammene til objektet properties som ligger på samme nivå. Skjemaet under er et grovt utgangspunkt på en beskrivelse av ansattobjektet.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://progit.no/employee",
"title": "Employee",
"description": "Employee of Progit Consulting",
"type": "object",
"additionalProperties": false,
"properties": {
"firstname": {
"type": "string",
"description": "The first name of the employee"
},
"middlename": {
"type": "string",
"description": "The middle name of the employee"
},
"lastname": {
"type": "string",
"description": "The last name of the employee"
},
"birthday": {
"type": "string",
"format": "date"
},
"salary": {
"type": "integer"
},
"contract": {
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
},
"address": {
"type": "string"
},
"startDate": {
"type": ["string", "null"],
"format": "date"
},
"endDate": {
"anyOf": [
{"type": "string"},
{"type": "null"}
],
"format": "date"
},
"skills": {
"type": "array"
}
}
}
På rotnivå har det blitt lagt til to nye felter, title
og description
. Dette er i likhet med $id
og $schema
metadata, men er noe mer selvforklarende. Properties objektet blir fylt med objekter som representerer de feltene objektet skal inneholde. Dersom du ønsker at feltene angitt i properties skal være utfyllende så kan du også legge til feltet "additionalProperties": false
. Da vil felter som ikke finnes i properties forårsake at skjemavalideringen feiler. Dette feltet er satt som true
som default.
I skjemaet over kan du skimte at feltene firstname, middlename og lastname er satt til å være string. Her blir nemlig type
brukt igjen, men et annet sted enn i vårt aller første eksempel. type
er et nøkkelord i JSON Schema og brukes til å beskrive ethvert felt, uansett hvor i objekthierarkiet det er plassert. Disse feltene har også blitt gitt feltet description. Dette ble gitt for å demonstrere at det er mulig å dekorere feltene med metadata.
Hva om feltet kan være flere typer?
Hva gjør man dersom man har et felt som kan være én av potensielt flere typer? Dette demonstreres ved et eksempel mot bunnen av skjemaet. Feltene startDate og endDate kan være enten string
eller null
. Det er demonstrert to forskjellige måter å gjøre det på. For startDate så er type
en liste (array
) med potensielle typer. For endDate så introduseres et nytt JSON Schema nøkkelord, anyOf
.
Nøkkelordet anyOf
krever en liste med potensielle verdier. Skjemaet er gyldig dersom et av disse feltene stemmer overens med dataen. Det gir mening i ordlyden. Sammen med anyOf
finnes nøkkelordene oneOf
, allOf
og not
. Kombinasjonen av disse nøkkelordene bidrar til at man kan komponere avansert logikk i skjemaet.
Helt nederst har vi feltet skills. Dette feltet har type array
. Det vil godta alt så lenge det er en liste. For å spesifisere til skjemaet hva som er lovlig innhold i listen kan man beskrive det slik:
"skills": {
"type": "array",
"contains": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"proficiency": {
"type": "string"
}
}
}
}
Her beskriver vi innholdet i arrayet ved nøkkelordet contains
.
Referanse til andre skjemaer
Hva om samme objekt skal valideres flere steder i applikasjonen? Som ellers i programvareutvikling så ønsker man å følge DRY-prinsippet (Don't Repeat Yourself). Vi ønsker så mye gjenbruk som mulig og derfor bør man strukturere skjemaene slik at det støttes opp til gjenbruk. For eksempel så kan det være et behov for å validere ansatt-skjemaet som vist tidligere i artikkelen alene og i en liste. For å unngå å definere de samme verdiene i to skjemaer så kan vi referere til ansatt-skjemaet i et nytt skjema som representerer en liste over ansatte. Dette er hvor $id
kommer inn i bildet. Dette er en unik identifikator til skjemaet. Lag et nytt skjema som skal inneholde liste over ansatte:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://progit.no/employee-list",
"title": "Employee list",
"description": "Employee list of Progit Consulting",
"type": "array",
"contains": {
"$ref": "/employee"
}
}
I dette skjemaet blir vi introdusert for et nytt nøkkelord, nemlig $ref
. Dette tolkes av JSON Schema som en referanse til et annet skjema. Stusser du over at vi ikke gir den hele URI'en til vårt opprinnelige skjema? JSON Schema leter automatisk etter skjemaer som deler samme rot-URI, i dette tilfellet https://progit.no. For å gjenskape dette i nettleserversjonen så kan man opprette nye tabs i venstre kolonne. Alle skjemaene blir kompilert til valideringsmotoren, men det aktive skjemaet er det som er skrevet i taben med navn 'instance'.
Avslutning
I denne artikkelen har det blitt gitt et raskt overblikk av JSON Schema. Dette er bare noen smuler av funksjonaliteten til JSON Schema og dersom man ble nysgjerrig så kan man kanskje lese videre på egenhånd. Hvis interessen er stor nok så kanskje det åpner for ytterlige artikler om temaet med eksempler. Desto flere use-caser jeg har løst med JSON Schema desto mer glad blir jeg i det. For dere som kom hele veien ned hit, takk for at du tok deg tid til å lese!
Dersom du lurer på noe, feedback, eller ser en feil. Ikke nøl med å kontakt meg på oya@progit.no God jul