Rekapitulace
V minulém článku jsme se podívali na tento kus kódu:
print("{ \"id\": \"" + $id + "\", \"name\": \"" + $name + "\" }");
Dospěli jsme k závěru, že má několik problémů. Zdrojem těchto problémů je, že nedělá to, co programátor zamýšlel. Ty problémy pak mohou například umožnit zneužití tohoto kódu zlým aktérem k provedení akcí, které by neměl být schopen udělat. Některé z nich jsou neškodné, jiné už tolik ne. V této části této série se podíváme na to, jak tento kód opravit. Ale nejprve trochu odbočíme.
Co takhle validace?
Minule nám hlavní potíže působilo to, že se v uživatelském vstupu nacházely znaky, které jsou v kontextu JSON dokumentů nějakým způsobem významené. Typicky hlavně uvozovky, ale potenciálně i zpětná lomítka, nové řádky a další. Takže první myšlenka, která nám může přijít na mysl, je, že bychom měli nějakým způsobem „validovat“ vstup, tedy zakázat v něm znaky, které by mohly způsobit problémy. Přece jen, proč by někdo měl mít vlastní jméno s uvozovkami?
S tímto přístupem ale příliš daleko nedojdeme. V první řadě, uvozovky a zpětná lomítka nejsou ty jediné znaky, které nám budou dělat problémy. Ten stejný údaj může také procházet spoustou dalších systémů v různých formách. Měli bychom zakázat i menšítka, většítka a ampersandy, protože se může objevit v XML dokumentech? A co lomítka, otazníky a hashe, protože se může objevit v URI? A co čárky, protože se může objevit v CSV dokumentech? A co apostrofy, dvojtečky, hranaté závorky a složené závorky, protože se může objevit v YAML dokumentech? A co procenta a podtržítka, protože se může objevit v SQL dotazech? A co aritmetické operátory a závorky, protože se může objevovat v buňkách Excelové tabulky?
Obzvlášť u velkých a složitých systémů existuje reálná šance, že jeden údaj skrz všechny tyto formáty procházet bude. A postupně, jak se takový systém vyvíjí, se budou objevovat další a další. Dostali bychom se tedy do bodu, kdy bychom zakázali ohromné množství znaků či dokonce celých řetězců. Ty by nebylo možné používat v rámci daného údaje i v případě, kdy by jejich použití bylo zcela legitimní. To nám nemusí přehnaně vadit u jmen (ačkoliv např. pomlčka je u jmen poměrně běžná), ale co jiné druhy údajů? Adresy mohou obsahovat čárky, tečky, lomítka, pomlčky a další. Čísla bankovních účtů mohou obsahovat pomlčky a lomítka. Ale hlavně, co souvislý text? Tento článek samotný by nebylo možné napsat, pokud by procházel systémem, který se pomocí validace snaží eliminovat znaky, které by někde po cestě mohly být problematické.
Validace dat je samozřejmě důležitá. Ale účelem validace dat je zařídit, aby daná data dávala smysl a aby byla zpracovatelná. Aby jméno vypadalo jako jméno, adresa jako adresa a číslo bankovního účtu jako číslo bankovního účtu. Účelem validace by ale nemělo být suplovat neschopnost našeho programu korektně reprezentovat znaky v různých datových formátech.
Validace tedy nebude ta cesta, kterou se budeme dále vydávat. Než se ale vydáme tou správnou cestou, tak ještě jednou mírně odbočíme.
Co je to číslo?
Zapomeňme zatím na složité věci jako jsou JSON dokumenty a zamysleme se nad něčím jednodušším. Je toto číslo?
6
Ano? Ne? Možná? Zkusme další. Je toto číslo?
šest
A co toto?
VI
Nebo toto?
Do určité míry můžeme tvrdit, že ani jedna věc výše není číslo. Čísla můžeme považovat za abstraktní koncept, který nemá žádnou konkrétní manifestaci, na kterou můžeme ukázat a říci „toto je číslo“. Můžeme je sice reprezentovat různými způsoby – pomocí číslic, slov, obrázků, diagramů, množin a tak dále, ale to jsou všechno vskutku pouze reprezentace, které jsou v různých kontextech užitečné, ale nejsou to čísla samotná.
Počítače jsou docela dobré v práci s čísly. No, ne se skutečnými čísly, protože nemůžeme uložit abstraktní koncept do počítače, ale místo toho s reprezentacemi čísel. Mají samozřejmě svá omezení – obvykle používáme binární celá čísla s pevnou velikostí, která mohou reprezentovat jen velmi omezenou podmnožinu všech celých čísel. Také používáme čísla s plovoucí desetinnou čárkou dle IEEE 754, která předstírají, že jsou reálnými čísly, ale ve skutečnosti ani zdaleka nejsou.
A tak i když výraz 42
v jazyce C ve skutečnosti znamená něco jako „Chci, aby tento výraz byl nejlepší aproximací čísla 42, kterou mi můžeš dát, vzhledem k omezením typu int
, která jsou dána hardwarem, pro který tento kód kompiluješ,“ obvykle předstíráme, že znamená spíše „Chci, aby tento výraz byl číslo 42.“
Programovací jazyk nám pak poskytuje operace, které můžeme použít k manipulaci s těmito falešnými čísly – sčítání, odčítání, násobení, dělení a tak dále. A pokud zůstáváme v mezích těchto falešných čísel, můžeme předstírat, že pracujeme se skutečnými čísly, což je obvykle pro nás dostačující. Některé jazyky navíc kontrolují, že v rámci daných mezí vskutku zůstáváme a znemožňují nám provést operace, které by dané meze porušily – např. násobení dvou opravdu velikých čísel v rámci checked
bloku v jazyce C# vyústí ve vyhození výjimky System.OverflowException
.
Klíčovým bodem zde je, že kód, který manipuluje s čísly, s němi manipuluje pomocí operací, jejichž význam je zjevný programovacímu jazyku i programátorům. Ať už těm, co daný kód píšou, či těm, co daný kód čtou. Jednoduše řečeno – programátorovi i programovacímu jazyku je zjevné, že výraz a * 10
násobí číslo deseti. U výrazu parseInt(a + "0")
už to tak zjevné není a dokonce to ani není pravda – pokud řetězcová reprezentace a
obsahuje desetinnou čárku, tak k násobení deseti nedojde.
Nyní se tedy vraťme k našemu hlavnímu problému a pokusme se obdobné myšlení aplikovat na JSON dokumenty.
Co je to JSON dokument?
Podívejme se na toto. Je to JSON dokument?
{ "name": "Jason", "age": 42, "hobbies": [ "programování", "pití kávy" ], "contact": { "email": "jason@example.com", "phone": "123456789" } }
A co toto?
Opět můžeme do určité míry tvrdit, že ani jedna z těchto věcí není skutečný JSON dokument.
Je sice pravda, že ta první reprezentace je kanonická textová reprezentace podle RFC 8259, takže tvrdit, že to „není skutečný JSON dokument“, zní trochu hloupě. Ale i RFC 8259 rozlišuje „JSON“ a „JSON text“, kde „JSON text“ je textová reprezentace JSONu. Úvod dokonce definuje abstraktní představu JSON dokumentu, aniž by kdekoliv zmínil textovou reprezentaci. Volně přeloženo z angličtiny:
JSON může reprezentovat čtyři primitivní typy (řetězce, čísla, booleovské hodnoty a null) a dva strukturované typy (objekty a pole).
Objekt je neuspořádaná kolekce nula nebo více dvojic jméno/hodnota, kde jméno je řetězec a hodnota je řetězec, číslo, booleovská hodnota, null, objekt nebo pole.
Pole je uspořádaná posloupnost nula nebo více hodnot.
Toto je sice trochu nedospecifikované (např. žádným způsobem to nespecifikuje, co je to řetězec nebo číslo), ale i tak na to můžeme nahlížet jako na správný mentální model pro práci s JSON dokumenty. Tedy, JSON dokumenty jsou tvořeny z objektů, polí, řetězců, čísel, booleovských hodnot a nullů. Nejsou tvořeny ze složených závorek, hranatých závorek, dvojteček, čárek, uvozovek a písmen.
V předchozí části jsme jako příklad uvedli výraz a * 10
, kde je zjevné, že násobí číslo deseti. Pokud na JSON dokumenty budeme nahlížet podobně, tak bychom chtěli dosáhnout stavu, kdy náš kód, který manipuluje s JSON dokumenty, bude podobně zjevný programátorům i programovacímu jazyku.
Podívejme se znovu na onen původní kód, se kterým jsme začali:
print("{ \"id\": \"" + $id + "\", \"name\": \"" + $name + "\" }");
Hlavní operace v tomto kusu kódu je konkatenace řetězců. Tedy, nahlížíme na generovaný JSON dokument jako na posloupnost znaků a tvoříme ho spojováním několika různých řetězců. My už ale víme, že JSON dokumenty nejsou sekvence znaků. Měli bychom tedy nahlížet na generovaný dokument dle naší abstraktní představy o JSON dokumentech – v tomto případě se jedná o objekt s dvěma řetězcovými hodnotami. A chceme, aby náš kód reflektoval tuto abstraktní představu.
Zatímco většina programovacích jazyků má vestavěný operátor pro násobení čísel, tak vestavěné operace pro tvorbu JSON objektů už nebývají tak časté. Ale to neznamená, že nelze nic takového v daném jazyce postavit. Ve skutečnosti pravděpodobně existuje knihovna pro váš oblíbený programovací jazyk, která přesně tohle dělá. V některých případech může být dokonce součástí standardní knihovny.
Konečně se tedy podívejme na několik příkladů toho, jak bychom náš původní kus kódu v různých programovacích jazycích mohli opravit. Nebudeme zacházet do příliš hlubokých detailů, místo toho půjdeme cestou nejmenšího odporu.
Python
Začněme něčím jednoduchým. Python má ve své standardní knihovně modul json
. Tento modul většinou pracuje tak, že reprezentuje JSON dokumenty pomocí větavěných typů Pythonu (slovníky pro objekty, seznamy pro pole, řetězce pro řetězce, inty a floaty pro čísla atd.) a pak je převádí na kanonický JSON text a zpět.
Náš první pokus může vypadat takto:
import json print(json.dumps({ "id": $id, "name": $name }))
Jednoduše vytvoříme slovník s příslušnými klíči a hodnotami, který odpovídá JSON objektu, který chceme vygenerovat. Poté použijeme funkci dumps
k převodu na JSON text a ten vypíšeme.
JavaScript
Vzhledem k tomu, jak JSON vzniknul, můžeme očekávat, že v JavaScriptu bude práce s JSON dokumenty poměrně jednoduchá. A v našem případě tomu tak vskutku je:
console.log(JSON.stringify({ id: $id, name: $name }));
Tohle by nám mělo být povědomé. Tenhle kud kódu je prakticky identický tomu, co máme výše v Pythonu – jednoduše vytvoříme JavaScriptový objekt, který odpovídá našemu JSON dokumentu, ten převedeme do JSON textu pomocí JSON.stringify
a následně jej vypíšeme. Koncepčně tedy velice podobné.
C#
Balíček Json.NET byl po dlouhou dobu de facto standardem pro manipulaci s JSONem v C#. Nicméně nedávno se stal součástí standardní knihovny jmenný prostor System.Text.Json, který zvládne spoustu stejných úloh jako Json.NET. Nejsme tu proto, abychom je porovnávali, takže prozatím použijeme System.Text.Json. Jejich rozhraní jsou navíc pro základní případy použití velmi podobná.
Začněme tedy úplně jednoduše:
using System; using System.Text.Json; Console.WriteLine(JsonSerializer.Serialize(new { id = $id, name = $name }));
Tento kód je koncepčně velmi podobný výše uvedenému Python a JavaScript kódu, jen používáme anonymní objekt místo slovníku. Ale vzhledem k tomu, že C# je zpravidla více typovaný, než Python či JavaScript, tak by možná bylo vhodné použít záznam k zafixování schématu našeho JSON dokumentu:
using System; using System.Text.Json; Console.WriteLine(JsonSerializer.Serialize(new Person( id: $id, name: $name ))); record Person(string id, string name);
C
Všechny výše uvedené jazyky měly nějakou formu vestavěné podpory pro JSON. Nyní se podívejme na jazyk, který ji nemá. C nemá ani vestavěné slovníky, které by byly užitečné pro modelování JSON objektů.
V tomto případě použijeme externí knihovnu, která nám pomůže. Existuje několik populárních C knihoven pro práci s JSONem, jako například json-c a Jansson. Zde použijeme Jansson, ale není pro to žádný zvláštní důvod. Začněme tedy kompletním řešením:
#include <stdio.h> #include <jansson.h> int main() { json_t *json = json_object(); if (!json) goto fail_object; if (json_object_set_new(json, "id", json_string($id))) goto fail; if (json_object_set_new(json, "name", json_string($name))) goto fail; if (json_dumpf(json, stdout, 0)) goto fail; json_decref(json); return 0; fail: json_decref(json); fail_object: return 1; }
Je trochu typické pro kód v jazyce C, že zpracování chyb zakrývá to, co se opravdu děje. Ale pokud tento kus kódu zredukujeme na to hlavní, tak dostaneme něco takového:
json_t *json = json_object(); json_object_set_new(json, "id", json_string($id)); json_object_set_new(json, "name", json_string($name)); json_dumpf(json, stdout, 0); json_decref(json);
To není ani tak špatné. Vytvoříme JSON objekt, přidáme do něj dvě pole, vypíšeme ho na standardní výstup a pak ho uvolníme. Je to velice imperativní postup, ale dost explicitně popisuje to, čeho chceme dosáhnout. Určitě je to zjevnější, než by byla manipulace řetězců ekvivalentní našemu původnímu kusu kódu. Jen si musíme dát pozor na uvolnění zdrojů a zpracování chyb.
Jak jsme situaci zlepšili?
Jak už víme, tak náš původní kód nahlížel na generovaný JSON dokument jako na sekvenci znaků a manipuloval s nimi pomocí operací, které jsou vhodné pro manipulaci se sekvencí znaků, ale nejsou vhodné pro manipulaci s JSON dokumenty. V našich opravených příkladech výše místo toho nahlížíme na generovaný JSON dokument jako na abstraktní koncept tvořený, v našem případě, z objektu a dvou řetězců. A manipulujeme s ním pomocí operací, které tento abstraktní koncept respektují a které se shodují s tím, co programátor zamýšlí.
Ale nejdůležitější otázka je, zda jsme se zbavili problémů, které jsme si nastínili minule. V rychlosti si je projděme jeden po druhém:
- Neplatný JSON text již nelze vytvořit. Náš příklad z minula, kdy uživatel zadal
John "The Hacker" Doe
jako svoje jméno, vyústí v následující korektní JSON text:
{ "id": "1", "name": "John \"The Hacker\" Doe" }
- Přidávat dodatečná pole již také nelze. Náš příklad z minula, kdy uživatel zadal
John Doe", "role": "admin
jako svoje jméno a přidal tak do výsledného objektu polerole
, vyústí v následující korektní JSON text:
{ "id": "1", "name": "John Doe\", \"role\": \"admin" }
- A nakonec, přidávat další dokumenty již také nelze. Náš příklad z minula, kdy uživatel zadal
John Doe"}{ "id": "2, "name": "Jane Doe
jako svoje jméno a vytvořil tak dva objekty za sebou, vyústí v následující korektní JSON text:
{ "id": "1", "name": "John Doe\"}{ \"id\": \"2\", \"name\": \"Jane Doe" }
Všechny tyto situace samozřejmě předpokládají, že výrazy $id
a $name
jsou řetězce. Zařídit, že tomu tak je, je zpravidla záležitost buď typového systému a nebo aplikační logiky. Každopádně, vypadá to, že jsme se zbavili všech problémů, které nás trápily minule.
V konečném důsledku všechny naše příklady výše fungují dost podobně – vytvoříme v paměti nějakou reprezentaci našeho žádoucího JSON dokumentu a poté ji převedeme na JSON text. Ona reprezentace JSON dokumentu je v každém případě jiná – v některých případech jsou to vestavěné typy (slovník v Pythonu, objekt v JavaScriptu), naše vlastní typy (záznam v C#) či typy navržené přímo pro reprezentaci JSON objektů ( json_t
z knihovny Jansson v C), ale to je spíše detail – často je totiž možné tyto přístupy kombinovat. Můžeme naučit modul json
v Pythonu pracovat s našimi vlastními typy pomocí tříd JSONEncoder
a JSONDecoder
. Můžeme použít System.Text.Json
v C# pro práci se slovníky a seznamy a můžeme použít jeho třídu JsonNode
pro práci s JSON dokumentem jako stromem. Můžeme použít metody toJSON
a/nebo parametr replacer
ve funkci JSON.stringify
v JavaScriptu k úpravě výstupu všemi možnými podivnými způsoby. Možností je spousta a můžeme je kombinovat podle toho, co je pro danou situaci nejvhodnější.
Generování JSON textu podobným způsobem, ale v jiných jazycích či za použití jiných nástrojů, by pro nás nyní mělo být poměrně přímočaré. Budeme většinou používat stejné myšlenky, jen detaily budou odlišné. Například v Javě pravděpodobně budeme používat knihovnu Jackson. V Rustu pravděpodobně budeme používat knihovnu serde. V Haskellu pravděpodobně budeme používat knihovnu aeson. A tak dále.
Nutno poznamenat, že tento přístup, tedy serializace, není jediným možným. Ale těmi dalšími si zatím nebudeme lámat hlavu.
Náš kód je opravený a čitelný
Myslím, že jsme to dokázali. Opravili jsme svůj původní kus kódu. Byli jsme schopni jej nahradit kusem kódu, který skutečně sděluje náš záměr – jak počítači, tak dalším programátorům, kteří mohou tento kód v budoucnu číst. Už nemusíme hádat, že kód vytváří JSON dokument, protože je nyní zřejmé, že to dělá. Přidání dalšího pole do dokumentu je nyní triviální a ne bolestivé cvičení v počítání uvozovek, čárek a zpětných lomítek.
JSON dokumenty pochopitelně nejsou to jediné, u čeho se můžeme dostat do podobných problémů. Příště se podíváme na další strukturované formáty a pokusíme se na ně aplikovat podobný způsob myšlení.
(Autorem obrázků je Zdeněk Biberle.)