Java >> Java tutorial >  >> Tag >> Json

JSON Schema og Schema Validation i Clojure

Du har sikkert hørt om og kan endda have brugt XML Schema eller Document Type Definitions til at beskrive strukturen af ​​dine XML-filer, for at få autofuldførelse i din foretrukne IDE eller til at validere dine HTML-filer (i det mindste til en vis grad med HTML 5). Selvom dette har hjulpet os meget de sidste år, forventer eller returnerer mange konfigurationsfiler og REST-lignende API'er i dag JSON, og som det viser sig, er skemaer stadig nyttige.

En mulig use case for JSON-skemaer er valideringen af ​​brugerleveret JSON. Mens udviklere i den statisk indtastede sprogverden regelmæssigt bruger objektkortlæggere til at kortlægge JSON-datastrukturer til klasser og dermed validere strukturen af ​​de leverede data, bruger udviklere på sprog som JavaScript, Ruby og Clojure ofte en meget enklere tilgang. På sådanne sprog deserialiserer du sædvanligvis JSON til sprogenes ækvivalente datastrukturer, dvs. højst sandsynligt kort og lister, og fortsætter med at arbejde med disse datastrukturer. Meget simple applikationer vil derefter gå videre og lægge de brugerleverede data direkte ind i en eller anden database. Det burde være indlysende, at det er en dårlig idé at gøre det, men sådanne ting sker alt for ofte (for eksempel var GitHubs massetildelingsproblem faktisk ret ens).

Et simpelt JSON-skema

Lad os overveje et meget simpelt JSON-skema til et billede. Et billede kunne repræsenteres som et simpelt objekt med de fire egenskaber id , name , width og height . Derudover vil vi have name egenskab, der skal kræves, og brugeren bør ikke være i stand til at definere yderligere egenskaber. Følgende liste specificerer vores opfattelse af et billede.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "title": "image",
  "description": "Image representation",
  "properties": {
    "id": { "type": "string" },
    "name": { "type": "string" },
    "width": { "type": "integer" },
    "height": { "type": "integer" }
  },
  "required": ["name"],
  "additionalProperties": false
}

{ "$schema":"http://json-schema.org/draft-04/schema#", "type":"object", "title":"image", "description":"Billedrepræsentation", "egenskaber":{ "id":{ "type":"streng" }, "navn":{ "type":"streng" }, "bredde":{ "type":"integer" }, "højde" :{ "type":"integer" } }, "required":["navn"], "additionalProperties":false}

JSON-skemaer ser bemærkelsesværdigt enkle ud, men lad os skille dem ad fra toppen.

  • $schema egenskaben definerer, hvilken version af JSON Schema-specifikationen dette skema skal overholde. Udkast offentliggøres som IETF-arbejdsdokumenter på json-schema.org og på IETF-webstedet. Til formålet med dette blogindlæg er specifikationerne for JSON Schema kerne og JSON Schema validering tilstrækkelige (bare rolig, du behøver ikke at læse dem).
  • Hvert objekt kan være en af ​​de syv definerede primitive typer. object svarer til det, du typisk kender som en hash eller et kort. JSON Schema definerer en heltalstype, hvilket er ret interessant, da denne type ikke er en del af JSON-kernespecifikationen.
  • title og description kan bruges til at dokumentere typen og/eller til at give yderligere information til læseren. Egenskabernes værdier er ikke af interesse for en JSON-validator.
  • properties er en speciel egenskab for skemaer med typen object . Det er grundlæggende en rekursiv datastruktur, hvor hver nøgle ligner et gyldigt egenskabsnavn, og værdien er et JSON-skema. I tilfælde af vores eksempel har vi fire meget simple egenskaber, der kun definerer egenskabernes typer. Det behøver dog ikke at slutte her. Du kan gå amok og definere regulære udtryksregler for strenge, min og maks. værdier eller tal eller endda definere tilpassede typer.
  • Gennem required egenskaber, vi kan definere, ja, nødvendige egenskaber. Påkrævet betyder, at et objekt mindst skal have de nødvendige nøgler for at blive betragtet som gyldige.
  • additionalProperties definerer, om brugeren må definere flere egenskaber end dem, der er defineret i properties og patternProperties . Vi indstiller dette til falsk for at håndhæve vores objektstruktur. Som standard kan brugeren definere flere egenskaber end dem, der er angivet i vores skema .

En anden interessant funktion er muligheden for at have referencer i skemaer. Du kan bruge denne funktion til at referere til en del af dit skema eller endda til at referere til andre filer. Lad os for eksempel antage, at vores billedskema ligger i en fil kaldet image.json og at vi ønsker at definere en samling billeder i en fil kaldet collection.json . Følgende liste viser dig, hvordan du gør dette.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "title": "collection",
  "description": "Detailed collection representation",
  "properties": {
    "name": { "type": "string" },
    "description": { "type": "string" },
    "images": {
      "type": "array",
      "items": {
        "$ref": "image.json"
      }
    }
  },
  "required": ["name"],
  "additionalProperties": false
}

{ "$schema":"http://json-schema.org/draft-04/schema#", "type":"object", "title":"collection", "description":"Detaljeret samlingsrepræsentation" , "properties":{ "name":{ "type":"string" }, "description":{ "type":"string" }, "images":{ "type":"array", "items" :{ "$ref":"image.json" } } }, "required":["navn"], "additionalProperties":false}

Listen indeholder en ny ejendomstype, som du ikke har set før:arrays. Du kan definere acceptable varetyper for arrays. Igen kan dette være et JSON-skema eller, i dette tilfælde, en reference til en JSON-fil, som indeholder et JSON-skema. JSON-referencer er defineret i et separat IETF-arbejdsdokument.

Validering

Mens JSON Schema er meget nyttigt til dokumentation af API'er og konfigurationsfiler, finder jeg valideringen af ​​brugerinput som særlig værdifuld. Validatorer findes for en række forskellige sprog. I dette blogindlæg bruger jeg Clojure og json-schema-validator-biblioteket (som blot er et almindeligt Java-bibliotek).

Lad os starte enkelt og oprette en JsonSchemaFactory . Denne fabrik opretter JsonSchema instanser, der faktisk er ansvarlige for dokumentvalideringen.

(def
  ^{:private true
    :doc "An immutable and therefore thread-safe JSON schema factory.
         You can call (.getJsonSchema json-schema-factory )
         to retrieve a JsonSchema instance which can validate JSON."}
  json-schema-factory
  (let [transformer (.. (URITransformer/newBuilder)
                        (setNamespace "resource:/schema/")
                        freeze)
        loading-config (.. (LoadingConfiguration/newBuilder)
                           (setURITransformer transformer)
                           freeze)
        factory (.. (JsonSchemaFactory/newBuilder)
                    (setLoadingConfiguration loading-config)
                    freeze)]
    factory))

(def ^{:private true :doc "En uforanderlig og derfor trådsikker JSON-skemafabrik. Du kan kalde (.getJsonSchema json-schema-factory ) for at hente en JsonSchema-instans, som kan validere JSON."} json-schema-factory (lad [transformer (.. (URITransformer/newBuilder) (setNamespace "resource:/schema/") fryse) loading-config (.. (LoadingConfiguration/newBuilder) (setURITransformer transformer) fryse) fabrik (.. (JsonSchemaFactory/newBuilder) (setLoadingConfiguration loading-config) freeze)] fabrik))

Som du kan se, er vi nødt til at konfigurere fabrikken på en speciel måde, så den kan løse refererede skemafiler. Du kan gøre det gennem en URITransformer (JSON-referencer er almindelige URI'er). Denne transformer vil kun blive konsulteret for refererede skemafiler, som du vil se senere.

Dernæst er nogle hjælpefunktioner, som vi bruger til at indlæse skemafilen fra klassestien og til at konvertere den til JsonNode forekomster.

(def
  ^{:private true
    :doc "Initialize the object mapper first and keep it private as not all
         of its methods are thread-safe. Optionally configure it here.
         Reader instances are cheap to create."}
  get-object-reader
  (let [object-mapper (ObjectMapper.)]
    (fn [] (.reader object-mapper))))
 
(defn- parse-to-node
  "Parse the given String as JSON. Returns a Jackson JsonNode."
  [data] (.readTree (get-object-reader) data))
 
(defn- get-schema
  "Get the schema file's contents in form of a string. Function only expects
  the schema name, i.e. 'collection' or 'image'."
  [schema-name]
  (slurp (io/resource (str "schema/" schema-name ".json"))))

(def ^{:private true :doc "Initialiser først objektmapperen og hold den privat, da ikke alle dens metoder er trådsikre. Konfigurer den eventuelt her. Læserforekomster er billige at oprette."} get-object-reader ( let [object-mapper (ObjectMapper.)] (fn [] (.reader object-mapper)))) (defn- parse-to-node "Parse the given String as JSON. Returnerer en Jackson JsonNode." [data] ( .readTree (get-object-reader) data)) (defn- get-schema "Få skemafilens indhold i form af en streng. Funktionen forventer kun skemanavnet, dvs. 'samling' eller 'billede'." [skema- navn] (slurp (io/ressource (str "schema/" skemanavn ".json"))))

Alle tre funktioner er ret standard. Vi har en hjælpefunktion get-object-reader for at oprette en Jackson ObjectReader eksempel. Vi har brug for denne og følgende funktion parse-to-node som JsonSchemaFactory 's getJsonSchema metoden forventer et parset JSON-skema. Endelig har vi en funktion get-schema for at indlæse en skemafils indhold fra klassestien.

(defn validate
  "Validates the given 'data' against the JSON schema. Returns an object
  with a :success property that equals true when the schema could
  be validated successfully. It additionally contains a :message property
  with a human readable error description."
  [schema-name data]
  (let [parsed-schema (parse-to-node (get-schema schema-name))
        schema (.getJsonSchema json-schema-factory parsed-schema)
        parsed-data (parse-to-node data)
        report (.validate schema parsed-data)]
    {:success (.isSuccess report)
     :message (str report)}))

(defn validate "Validerer de givne 'data' mod JSON-skemaet. Returnerer et objekt med en :success-egenskab, der er lig med true, når skemaet kunne valideres med succes. Det indeholder desuden en :message-egenskab med en menneskelig læsbar fejlbeskrivelse." [ skemanavn data] (lad [parsed-schema (parse-to-node (get-schema schema-name)) schema (.getJsonSchema json-schema-factory parsed-schema) parsed-data (parse-to-node data) rapport (.validate schema parsed-data)] {:success (.isSuccess-rapport) :message (str-rapport)}))

Den egentlige kerne i vores valideringslogik er validate fungere. Vi bruger de tidligere definerede funktioner til at hente og parse skemaet, forvandle dette skema til en JsonSchema parse de brugerleverede data og generere en valideringsrapport.

Hvis du er interesseret i den fulde kildekode, kan du finde dette blogindlægs eksempelkildekode på GitHub.

Oversigt

JSON-skema kan være nyttigt til strukturel validering af brugerleveret JSON. Selvom det er ret udtryksfuldt, kan JSON Schema ikke bruges til at udtrykke en hel række af semantiske valideringer. For sådanne valideringsregler skal du stadig falde tilbage til din foretrukne valideringsmekanisme. Ud over validering kan du bruge JSON-skemaer til at udtrykke dine API'er eller konfigurationsfilers strukturer. Førstnævnte kunne bruges sammen med værktøjer som Swagger eller RAML til at dokumentere en REST-lignende API.


Java tag