Валідны JSON, абяроўаны контракт: шаровыя пераказы для выявлення регрэсій у палёадзе.
Выучыце, як выяўляць регрэсіі ў JSON-паданні, якія парсуюцца правільна: семантычныя разліки, спецыфікацыя JSON Schema, асэрцыі бізнес-правіл у Node і меры дазволеных гэнераваных типаў.
Крыніцы: Catch JSON Contract Regressions Before They Reach Your App
Адказ можа прыйсці з кодам HTTP 200, будзе распакаваны без жадных адказоў пра бяды, але ўсё равна зламаць вашу прыкладненне. Перакананне сінтаксісу паўтарае толькі тое, што текст ёсць у формате JSON; гэта нічога не сказвае пра тое, чы роўнасць пакета дадзеных яшчэ адпавядае таму, чаго чакае ваш код. У гэтым карыце створваецца маленькі, шаравыяй працэс перагляду регрэсіі (семантычныя порэванні, спецыфічная схема і чысткія бізнес-правіла), таким чынам кожны нарушэння угоды выяўляецца як точны адказ, а не як незрозумелы баг у працэсе выкарыстоўвання.
Пакет дадзеных, які распакаваны, але ўсё равно не працуе
Разглянем службу адміністрування замовленняў. Канцэнтрык успешна адпавядае, JSON паспеліваеся, і тады код, які сумуўае колькасці айтэнаў, выдае "021" заместо 3. Гэты сценарый вымышлены, але спосаб неяўнасці є распашчасты: у перапоўненай паказцы строку "2" і цэлык 2 лёгка працягнуць, абодва ёсць дазволенымі значэннямі JSON, і толькі адна з іх павінна быць прыйнятаяя па даговору.
Наведзены выкладак прадстаўляе прыходзячае замовленне. Атрымайце дакладнаю увагу на першы айтэн: яго quantity — це строка з кавычкамі, тады як другі айтэн викорыстоўвае просты цэлык. Усё іншае є правдападобнымі данымі замовлення, укладаючы null для інструкцый пра доставку і заздалегідь вырачаны totalCents.
{
"orderId": "ORD-1042",
"createdAt": "2026-09-21T09:30:00Z",
"customer": {"id": "C-017", "name": "Avery Example", "vip": false},
"items": [
{"sku": "MUG-BLUE", "quantity": "2", "unitPriceCents": 1250},
{"sku": "NOTE-A5", "quantity": 1, "unitPriceCents": 650}
],
"shipping": {"city": "Exampleton", "instructions": null},
"paid": true,
"totalCents": 3150
}
Строгая пераконтрацыя сінтаксісу прыме гэты дакумент без жадных заўважэнняў, і гэта ёсць правамерны рэзультат: тэкст адпавядае граматыцы JSON у RFC 8259, дзе строкі, цыфры, логічныя значэння і null ўсе являюцься дзейнарамі значэнняў. Граматыка не можа знайсці правілы, якія прыменяе ваша аплікацыя:
quantityпавінен быць цэлым числам, большым за нольpaidпавінен быць логічным значэнням- сума кожной кантыяты, умножанай на ўзельную цэну, павінна дораўняваць
totalCents
Працюймо з гадкай, што пярэднія версія гэтага замовлення адрознілася толькі ў двух месцах: значэнне quantity было числам 2, а paid — false. Змена флага платежа на true ўскладніцца як стандартная змена стану. Адно ж значэнне quantity, якое з числа ператворылася ў строку, ёсць прыкладам регрэсіі.
Значэнні разлікі па шляху, а не па лініях тексту
Пры супэранаванні пэйлоадаў можна задаць два аблічны разных запытанні. Семантычны супэранаванак аналізуе оба документы і парэйнуе значэнні па кожным шляху. Ён спрыяе порядку ключоў об’екта як несутэчнаму, таму змена месца customer.id і customer.name не лягае ў базу змян, тады як порядак у масэвых структурах залічаецца значным, таму перасунуць элемент на іншы індэкс можа лягчыць у базу змян. Для гэтага фікстчу семантычны дыфф адзвярцает толькі два шляху: $.items[0].quantity і $.paid. Гэта адпавядае на запытанне "Калікі дадзеных змяніліся?"
Разлік текста адаптаваны да другаму пытанню: «Калі змяніліся символы чырае?» Ён таксама ўтварае корыстныя доказы, а стандартызаванне відступаў і форматавання на обох сторонах спачатку пазбавляе многа «шуму». Але зусім той самы чырае текст не ўтварае доказу таго, што два дакументы выражаюць або не выражаюць тое ж значэння JSON, калі пры этым можа разліцца прыводкі, порядак ключоў і форматаванне цыфр, не змінюючы при гэтым значэння.
Адтворэнне пераканальнікаў у простам Node
Вам не патрэбны жадныя зовнішнія сервісы для пераканання ў правядзібнасці гэтых фактов. Наведжаны нижэй скрыпт чытае файл order.json з сябеўскага каталогу (чераз import.meta.url), перадзначае пакой версію за дапамою structuredClone і пераканваецца ў наявнасці разніцы за дапамою node:assert/strict. Пасля чаго ён паказвае бяг, вылечвае прыклад на копіі і пераканваецца, што вырахаваная сума падходзіць да totalCents. Зберажыце яго як check-order.mjs празаўсюды ля прыкладу і запустіце node check-order.mjs.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const incoming = JSON.parse(
readFileSync(new URL('./order.json', import.meta.url), 'utf8')
);
const before = structuredClone(incoming);
before.items[0].quantity = 2;
before.paid = false;
assert.equal(before.items[0].quantity, 2);
assert.equal(incoming.items[0].quantity, '2');
assert.equal(before.paid, false);
assert.equal(incoming.paid, true);
const naiveCount = incoming.items.reduce(
(sum, item) => sum + item.quantity,
0
);
assert.equal(naiveCount, '021');
const corrected = structuredClone(incoming);
corrected.items[0].quantity = 2;
const count = corrected.items.reduce((sum, item) => sum + item.quantity, 0);
const computedTotal = corrected.items.reduce(
(sum, item) => sum + item.quantity * item.unitPriceCents,
0
);
assert.equal(count, 3);
assert.equal(computedTotal, corrected.totalCents);
console.log('Contract checks passed');
Цікавайшым ёсць элемент naiveCount. Аптаматызацыя пачынаецца з числа 0, дае да яго строку "2" і рэзультатам стае строка "02", тым чыто аператар + пераходзіць на спаўненне, калі хоць адны з операндов являе ся стракой. Наступны крок — дадаць 1, ітогам чаго атрымліваецца "021". Чыслаўскі пачатковы значэнне не прыводзіць да чыслаўскага формата наступных операндоў; як толькі ў аккумулятар увайшла строка, усе, што следуе за яю, спаўнюецца.
Зверніце ўвагу на тое, чаго скрыпт не рэкамендуе. Змена фікстчура на число — это спосаб выражэння правильнага контракту, а не падказка прыводзіць кожны прыходзячы поле да формата Number(). Безшумны прыводзіць до чыслаўскага формата маскіруе бяги ў кодзе, які гэта стварае. Трэба альбо пасправіць сервіс, які выдае строку, альбо задаць чытальнае, задокументаванае правіло нормалізацыі на месцы, дзе даныя прайшлі ў вашу систему.
Ператворэнне правіла типу ў схему
Правілы прозайскага тэксту лёгкая забываць; аднак правілы, якія можна выконваць, — ні. Схема Draft 2020-12, прыведзена нижэй, кодуе тыя вакансіі, якія маюць значэнне для гэтага регрэсійнага тэсту. Яна выкалічвае orderId, items, paid і totalCents, вымагае наявнасці прынеймна адного элемента, а таксама обмежвае quantity цэлым числам з мінімумам 1, а unitPriceCents — некацяным цэлым числам.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["orderId", "items", "paid", "totalCents"],
"properties": {
"orderId": {"type": "string"},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "quantity", "unitPriceCents"],
"properties": {
"sku": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"unitPriceCents": {"type": "integer", "minimum": 0}
}
}
},
"paid": {"type": "boolean"},
"totalCents": {"type": "integer", "minimum": 0}
}
}
Праверкі надходзячага дадзення па гэтай схеме збываюцца з паведамленнем у духу $.items[0].quantity: expected type integer. Змена гэтаго аднаго значэння на число 2 дазволяе праверкі працаваць. Гэты конкретны пат выступае самэй тым, што трэба аднавати пад час регрэсійных тэстаў.
Знайце, што насправды пакрывае ваш валідатор
Валідаторы разніцяюцца тым, насколькі з положэнняя спецыфікацыі яны рэалізуюць. Дзеярыя з іх навмысна прахідаюць аналіз заўантажваных цэльных элементаў $ref, вычысленне рэгулярных выразаў pattern чы абавязковасць паўнай спадабанасці да format; па значэнню стандарту format спрацьвоўваецца як анотацыя, а не як абавязак. Убедзіцеся, што валідатор, які вы вжываеце для тэставання, пакрывае тыя ж ключоўыя словы, на якіх базуецца ваша продакшн-спецыфікацыя, інакш схема можа выглядаць строгайшая, чым рэальныя перакананні, якія яе практыкуюць.
Случаі на межы, якія варта працаваць над кожным
Хорашая спецыфікацыя адхрошае некоректныя данні з разных прычын. Кожна з іх должна вызваць абракцэс сама па сабе:
- Полная відсутнасць элементаў
items(паляганне на правілеrequired) - Элементы
itemsяк порожняя маса (minItems) - Колькасць
quantityравная нулю (minimum)
paid адражаецца як стрынг "true" (type)Не менш важліва ўмова – гэта неаплацаваны замовленні з paid: false. Ключавая слова required пераканальваецца толькі наявнасцю этой властивасці, а не на тым, чыя яе значэнне ўсунутае. У офіцыйных нарадчыках JSON Schema пояснюецца, як required выкорыстоўваецца для аб’ектных властивасцяў і як кожны элемент масівы пераканальваецца па сабсхеме items.
Што схема навмесна не включае
Это цялевая схема, а не повна спецыфікацыя замовлення. Данні кліента, спосаб прызначэння, семантыка часовых метакаў і дапаможныя поля не маюць жадных абмежэнняў. Арытметычныя правіла таксама навольна не ўключаны: тое, чы рэчінгі складаюцца ў суму totalCents, являеся логікай прыемлі, яку стандартная JSON Schema не можа выразіць, і самэ гэта прычына, чаму скрыпт Node пераканана яе практыкуе окрема. Раздзелэнне на два слоі дапамагае кожнаму інструменту выкананы тое, у чым ён кращы.
Створаныя типы описваюць адны прыклад
Існуе спакуса запісаць пэйлоад у генератор JSON-у-TypeScript і спрыяць выходны результат як контракт. Працуючы з правільным фікстуром, такі генератор дае такія значэнняяк quantity: number, createdAt: string і instructions: null унутры вкладзіных типаў. Это точныя адзначэнні гэтага прыкладу і нічога больш:
- У JSON існуе толькі адзін тип — число, таму
numberне можа пазначаць "паўзіцый цэлык". - Адзін прыклад не можа паказаць, якія поля ўзельцаваныя.
- Поле, якое сёньдзе ў стане
null, завтра можа законна хаваць строку. - Тыпы TypeScript знікхаюць пад час выканання і ніколі не пераглядаюць прыйшлыя байты; у інструкцыі TypeScript пра асэртываннях типаў чыста сказана, што асэртыванні не выконваюць пераканання пад час выканання.
Якщо вы хочаце адзіну дэфініцыю, якая будзе кераваць як перакананням пад час выканання, так і статычнымі тыпамі, адзірніце нашу статэю пра раздзеленне аднаго шымату Zod між фронтэндам і бэкендам.
Точнасць таксама трэба прыменяць на граніцы
Цыфры ствараюць ўсё новую пастку. JavaScript парсуе {"id":9007199254740993} у 9007199254740992, таму што гэта значэнне перасягае Number.MAX_SAFE_INTEGER і ўсунута да найбліжэйшага можна выразіць двойковага числа. Якщо ідэнтыфікатор павінен захаваць усе цифры, то кантракт стваральніка должен яго апісаць як строку. Прабавы выправіць ситуацыю пасля JSON.parse ўжо запаздзелі; цифры вже зниклі.
Ключовыя выводы
- Зберагаюце апрацаваны прыклад вядомага, правільнага пэйлоаду і ніколі не викорыстоўвайце рэальныя аутентифікацыйныя даны чы ўсабістыя даны ў тэстовых дадзенах.
- Расследжванні трэба розглядаць як шары: строгая сінтаксіс, разлік семантычных значэнняў, верыфікацыя схемы, а пасля — бізнес-правіла, якія схема не можа выразіць.
- Генеруйце типы TypeScript толькі пасля таго, як кантракт будзе узгоджаны, і ніколі не плутайце іх з верыфікацыяй пад час выканання.
$.items[0].quantity быў зменены з цэлага на строку, таму перакананне не выйшла і адынаванне паўтарылася", а не "порядак выглядае некоректна".