1. JSON-документ = JSON-значення
Коли людина вперше бачить JSON, вона часто запам’ятовує його як «щось у фігурних дужках». Це нормальна стартова ілюзія, приблизно як думати, що інтернет — це «сторінки в браузері». Насправді JSON набагато простіший і водночас суворіший: JSON-документ — це одне JSON-значення. Так, інколи це значення виглядає як об’єкт {...} або масив [...], але «атом» JSON — саме значення, і значення має тип.
У JSON існує всього шість видів значень: рядок, число, булеве значення, null, об’єкт і масив. Сьогодні ми свідомо беремо лише «прості» (скалярні) типи: рядок, число, boolean і null. Вони трапляються буквально в кожному API: title і author — рядки, id і count — числа, isAvailable — boolean, а comment може бути null. І ось тут починається найцікавіше: тип значення визначається не тим, «як це читається словами», а тим, як воно записане.
Щоб відчути ідею, корисно побачити, що навіть такий «маленький» JSON теж валідний і є повноцінним JSON-документом:
// Кожен із цих літералів — окремий JSON-документ (рівно одне JSON-значення)
String jsonDoc1 = "true"; // булеве значення
String jsonDoc2 = "2008"; // число
String jsonDoc3 = "\"Clean Code\""; // рядок (лапки — частина JSON, тому їх екрановано в Java-рядку)
String jsonDoc4 = "null"; // null
Усі чотири рядки вище — валідні JSON-документи, кожен сам по собі. У реальному API так майже не роблять на верхньому рівні — там зазвичай об’єкт або масив, — але всередині об’єкта такі значення трапляються постійно. І якщо ви навчитеся швидко бачити тип значення, рівень тривоги під час читання реальних відповідей різко знизиться: ви розумітимете, де текст, де число, де прапорець, а де «значення немає».
Для наочності — маленька «карта місцевості» того, куди сьогодні вписуються наші типи:
flowchart TD V["JSON-значення"] --> S["Рядок"] V --> N["Число"] V --> B["Булеве значення"] V --> Z["null"] V --> O["Об’єкт"] V --> A["Масив"]
2. Рядки в JSON: лапки й екранування
Рядки — най«людяніший» тип у JSON. Назви книг, імена авторів, статуси на кшталт "PLANNED" — усе це рядки. І саме через звичність рядків багато новачків припускаються найприкріших помилок: використовують одинарні лапки, забувають про екранування або випадково перетворюють число на рядок, а потім дивуються, чому "10" поводиться не як 10. Тому зараз ми домовимося про кілька залізних правил, і жити стане спокійніше.
Перше правило: рядковий тип у JSON упізнається за подвійними лапками. Якщо ви бачите "Clean Code" — це рядок. Без лапок це вже не JSON-рядок, а одинарні лапки сюди не підходять.
Ось мінімальні приклади JSON-рядків як окремих JSON-документів:
// Усередині Java-рядка ми зберігаємо JSON-рядок, тому лапки JSON потрібно екранувати
String jsonTitle = "\"Clean Code\"";
String jsonAuthor = "\"Robert C. Martin\"";
// Друкуємо як є: побачите саме JSON-представлення з лапками
System.out.println(jsonTitle); // "Clean Code"
System.out.println(jsonAuthor); // "Robert C. Martin"
Зверніть увагу на «подвійну реальність»: ми пишемо Java-рядок, усередині якого лежить JSON-рядок. Тому лапки потрібно екранувати. Так, це виглядає як «лапки в лапках», і це нормально. Звикайте: у backend-розробці ви постійно описуєте одні дані всередині інших: HTTP у коді, JSON у HTTP, потім DTO всередині JSON… і так далі — по спіралі дорослішання.
Друге правило: усередині рядка інколи потрібно екранувати символи. Наприклад, якщо ви хочете передати лапку як частину тексту, у JSON вона має бути записана як \". Аналогічно, зворотний слеш — це \\. Приклад:
// Тут усередині JSON-рядка є лапки, тому вони записані як \" (а в Java-рядку — як \\")
String jsonWithQuotes = "\"He said: \\\"hi\\\"\"";
System.out.println(jsonWithQuotes); // "He said: \"hi\""
Щоб не перетворювати лекцію на курс «Переможи слеші», достатньо пам’ятати популярні escape-послідовності. Ось невелика табличка, яка зазвичай покриває 90% реальності:
| Що хочемо бачити всередині рядка | Як записати в JSON-рядку |
|---|---|
| " | \" |
| \ | \\ |
| перенесення рядка | \n |
| табуляція | \t |
І ще один практичний нюанс: у JSON немає окремого типу «символ» (як char у Java). Є лише рядок. Тому "A" — рядок довжини 1, і це нормально.
Якщо пов’язати це з нашим проєктом ReadLater Starter, то майже всі «людяні» поля будуть рядками: title, author, status, externalId, comment. Навіть якщо status виглядає як «перелічення», на рівні JSON це все одно рядок. Про дисципліну DTO ми поговоримо пізніше, але для читання контракту зараз важливо саме це.
3. Числа в JSON: формат і лапки
З числами в JSON усе здається простим рівно до першого реального API. Потім раптово з’ясовується, що 2008 і "2008" — це різні речі, що 3,14 — не число, бо кома, що 01 — підозрілий запис, і що «а чому мій id раптом став рядком» — це не філософське питання, а цілком конкретний біль інтеграції. Зараз розберемо базу без зайвої математичної академічності, але чесно.
Головне правило: число в JSON пишеться без лапок. Якщо ви бачите 2008 — це число. Якщо ви бачите "2008" — це рядок, хоча очима хочеться вигукнути: «Та це ж рік!». JSON не дивиться очима — він дивиться на синтаксис.
Міні-набір прикладів чисел як JSON-документів:
// Усередині Java-рядка зберігаємо JSON-число без лапок
String jsonYear = "2008";
String jsonCount = "0";
String jsonRating = "4.5";
String jsonNegative = "-1";
Так, дробова частина записується через крапку, а не через кому. JSON у цьому сенсі «інтернаціональний» і дотримується машинного формату, а не звичок конкретної людини та її клавіатури.
Іноді трапляється і науковий запис, особливо якщо JSON надходить із наукових або фінансових джерел. На рівні читання контракту корисно хоча б упізнати цю форму:
String jsonScientific = "1e3"; // науковий запис: це 1000
Тепер про дисципліну. У специфікації JSON числа описані доволі суворо: наприклад, провідні нулі на кшталт 01 — неприпустимі. Але на практиці ви як розробник робите дуже просту річ: домовляєтеся, які поля в контракті мають бути числами, і стежите, щоб вони не «з’їжджали» в рядки.
У ReadLater майже напевно будуть числа для ідентифікаторів і лічильників: id, count. Пізніше, коли ми віддаватимемо список, з’явиться count: 0, 1, 42. Ось тут важливо, щоб це були числа, а не рядки — інакше клієнту, хоч Postman-скрипту, хоч застосунку, доведеться робити зайві перетворення.
Найнаочніший приклад того, чому тип важливий, — сортування. Рядки сортуються як текст, а числа — як числа. У текстовому світі "10" «менше» "2", бо '1' іде раніше за '2'. У числовому світі 10 більше за 2. І якщо ви колись побачите дивне сортування або фільтрацію, перша думка має бути: «А ми точно не перетворили числа на рядки?».
До речі, якщо ви працюєте з JSON очима — у Postman або просто читаючи відповідь, — ви майже завжди можете відрізнити рядок від числа одним рухом: у рядка будуть лапки, у числа — ні.
4. Булеві значення в JSON: true/false без лапок
Булеві значення в JSON — це маленькі чесні прапорці. Вони ідеальні для полів на кшталт «увімкнено/вимкнено», «доступно/недоступно», «є/немає». Їх найпростіше читати очима, і саме тому їх інколи псують «покращеннями» найчастіше. Наприклад, хтось вирішує написати "True" з великої літери, хтось — "yes", хтось — "0" і "1". І ось тут уже починається цирк, який клієнти змушені розбирати вручну.
У JSON усе суворо: булеве значення — це лише true або false, обов’язково малими літерами й без лапок.
Міні-набір прикладів:
// Справжні boolean у JSON: лише true/false і тільки без лапок
String jsonTrue = "true";
String jsonFalse = "false";
// А це вже рядок, хоча виглядає як "схоже на false"
String jsonTextFalse = "\"false\"";
System.out.println(jsonTrue); // true
System.out.println(jsonFalse); // false
System.out.println(jsonTextFalse); // "false"
Третій приклад — це не boolean, а рядок зі словом false. Для людини може здаватися: «Ну, сенс зрозумілий», але для контракту це вже інший тип, і клієнт, який очікує boolean, матиме повне право сказати: «Вибачте, я це їсти не буду».
Де в ReadLater Starter можуть з’явитися boolean? У нашому навчальному домені їх небагато, але вони цілком можливі як службові прапорці. Наприклад, у /health (який буде пізніше) теоретично може бути healthy: true. Або у зовнішньому каталозі для книги може бути hasCover: true. Неважливо, яке саме поле — важливо, що щойно ми обираємо boolean, ми дотримуємося справжнього boolean, а не «псевдобулевого тексту».
Ще один нюанс: boolean у JSON — це саме логічний тип, а не «технічна економія символів». Тому ідеї «давайте зберігати прапорці як 0/1» найчастіше погіршують читабельність і додають перетворення на боці клієнта.
5. null в JSON: значення немає
null — це, мабуть, найкорисніше і найчастіше неправильно зрозуміле слово в JSON. Новачки часто сприймають його як «порожньо» в широкому сенсі: порожній рядок, нуль, відсутність поля — усе туди ж. Але на рівні API-контракту null означає дуже конкретну річ: значення явно відсутнє. Тобто ми не «забули» надіслати поле, а свідомо кажемо: «поле є, але значення в нього зараз немає».
У JSON null записується так і тільки так: null, без лапок.
// null у JSON — окреме значення, без лапок
String jsonNull = "null";
System.out.println(jsonNull); // null
У реальних API null особливо часто трапляється в опціональних полях. Наприклад, у ReadLater у книги може не бути externalId, якщо ми додали її вручну, а не з каталогу, або користувач міг не залишити коментар. Тоді трапляються такі фрагменти:
// Текстовий блок: зручно показати JSON без екранування лапок у Java-рядку
String jsonCommentNull = """
{ "comment": null }
""";
String jsonExternalIdNull = """
{ "externalId": null }
""";
Я спеціально показую це всередині об’єкта, щоб ви побачили «природний» контекст, але в цій лекції нам не потрібно розбирати сам об’єкт — лише значення null.
Важливо не переплутати null із «порожнім рядком» і «нулем». Якщо поле "comment": "", це означає: коментар є, він текстовий, але порожній. Якщо "count": 0, це означає: значення є, воно числове, і це нуль. Якщо "comment": null, це означає: коментаря як значення немає, або він невідомий, або не заданий — і це має пояснювати контракт.
І так, я знаю, що мозок просить: «Ну це ж усе за змістом майже одне й те саме». У API це не так. Клієнт, а за пару тижнів і ви самі, подякує контракту за точність, навіть якщо спочатку хочеться простіше.
Трохи закинемо «гачок» на наступну лекцію: null — це один стан, але є ще стан «поле відсутнє». Це інше. Ми докладно порівняємо їх пізніше, але вже зараз корисно тримати в голові: null — це явна відсутність значення.
6. Візуальний парсер за першими символами
Коли JSON стає довгим і вкладеним, дуже хочеться «схопити сенс» за секунду: де рядок, де число, де об’єкт, де масив. Гарна новина: JSON у цьому сенсі дружній. Тип значення майже завжди вгадується за першими символами, якщо ви дивитеся не на зміст слів, а на форму запису. Це одна з тих маленьких навичок, яка раптом робить читання API-відповідей спокійним.
Найпростіша евристика така: подивіться на перший значущий символ після пробілів і перенесень. Якщо це " — перед вами рядок. Якщо це цифра або - — найімовірніше число. Якщо це t або f — boolean. Якщо n — null. Якщо { — об’єкт. Якщо [ — масив.
Можна навіть зробити маленьку «пам’ятку» у вигляді таблиці:
| Перший символ після пробілів | Тип значення |
|---|---|
| " | рядок |
0…9 або |
число |
t / |
true / false |
| n | null |
| { | об’єкт (наступна лекція) |
| [ | масив (наступна лекція) |
Якщо хочеться закріпити це на рівні коду — просто як ілюстрацію, а не як справжній парсер, — можна написати мініфункцію «вгадай тип за виглядом» і побачити, що логіка справді елементарна:
static String guessJsonType(String json) {
// Прибираємо пробіли й перенесення: нам важливий перший значущий символ
String t = json.trim();
// Рядок у JSON завжди починається з подвійної лапки
if (t.startsWith("\"")) return "STRING";
// Булеве значення — лише true/false, без лапок
if (t.equals("true") || t.equals("false")) return "BOOLEAN";
// null — окреме значення, без лапок
if (t.equals("null")) return "NULL";
// За першими символами легко розпізнати об’єкт або масив
if (t.startsWith("{")) return "OBJECT";
if (t.startsWith("[")) return "ARRAY";
// Якщо це не схоже ні на що з переліченого, найчастіше це число (але тут ми його не перевіряємо)
return "NUMBER (або щось дивне)";
}
Ця функція не робить валідацію і не розуміє нюансів чисел — і нам це зараз не потрібно. Вона показує головне: JSON побудований так, щоб тип «читався» з форми, а не вгадувався за контекстом.
7. Типи й API-контракт
На рівні «читаю очима» типи здаються дрібницею. Але backend-розробка швидко вчить: контракт існує не для краси, а для того, щоб дві незалежні сторони могли надійно взаємодіяти. Клієнт не сидить у вас у голові й не вгадує, що ви мали на увазі. Він бачить конкретні байти й намагається перетворити їх на структуру даних. І якщо тип «поїхав», починається ланцюжок неприємностей.
Уявіть типовий сценарій для нашого майбутнього локального API: ви віддаєте список елементів reading list і поруч пишете count. Якщо ви повернули count як рядок ("count": "2"), то клієнт, який очікує число, або впаде з помилкою, або буде змушений робити перетворення. У Postman це теж видно миттєво: лапки навколо числа — немов червоний прапорець «ми щойно домовилися про різні речі».
Або інший сценарій: статус читання. Якщо ви повернули "status": "FINISHED" — це рядок, і клієнт може порівнювати його з очікуваними варіантами. Якщо ви повернули "status": true (так, таке теж буває в реальному житті, коли хтось «оптимізував» модель), то клієнт не зможе зрозуміти, що це означає: прочитано? Не прочитано? А де тоді IN_PROGRESS? Тобто проблема не лише в типі, а й у сенсі, який тип допомагає утримати.
Ще одне часте джерело болю — null. Для опціональних полів comment або externalId null — нормальний стан, але лише якщо він очікуваний за контрактом. Якщо клієнт думає, що там завжди рядок, а сервер інколи надсилає null, з’являються історії на кшталт NPE (у різних мовах це проявляється по-різному, але суть одна: «я очікував текст, а отримав відсутність значення»).
Тому правило «бачити тип» — це не теорія заради теорії. Це практичний захист від ситуації, коли ви ніби й «віддаєте правильні дані», але система розвалюється через те, що дані виявилися іншого типу. І що особливо важливо для нас у цьому курсі: поки ми не використовуємо жодних фреймворків, нам потрібно навчитися самостійно помічати такі проблеми очима ще до того, як з’являться зручні DTO та автоматичні мапінги.
8. Типові помилки з простими значеннями
Помилки з простими JSON-значеннями зазвичай підступні тим, що виглядають «майже правильно». Мозок домальовує сенс, і здається, що все гаразд — поки ви не спробуєте віддати це реальному клієнту, не відкриєте відповідь у Postman або не попросите програму розібрати дані. Нижче — найчастіші граблі, на які наступають новачки, і причини, чому ці граблі такі популярні.
Помилка №1: одинарні лапки замість подвійних у рядках.
Запис на кшталт 'Clean Code' часто виглядає «ну майже те саме». Але для JSON це вже не рядкове значення, а невалідний фрагмент. Щойно в рядках з’являються одинарні лапки замість подвійних, клієнту вже нічого розбирати без костилів.
Помилка №2: числа в лапках «про всяк випадок».
Дуже часта думка: «Ну хай буде рядком, так надійніше». У результаті 2008 перетворюється на "2008", count перетворюється на "0", і клієнт раптово не може виконувати числові операції без перетворення. Це ламає сортування, фільтри й просто додає тертя. Якщо значення числове за змістом контракту, воно має бути числом і за формою.
Помилка №3: булеві значення як рядки — "true" і "false".
Це майже завжди стається через логіку «я бачу слово, значить це рядок». Але в JSON boolean — окремий тип, і false відрізняється від "false". Якщо ви надішлете "false" замість false, клієнт може сприйняти це як «непорожній рядок», а значить — як «істину» в деяких мовах або перевірках, і вийде дуже веселий баг: «У мене false, але все одно працює як true».
Помилка №4: десяткова кома замість крапки в числах.
3,14 — звично людині, але не JSON. JSON використовує крапку: 3.14. Кома в JSON — це роздільник елементів, а не частина числа. Тому запис із комою робить документ невалідним або призводить до зовсім іншого розбору.
Помилка №5: null сприймають як порожній рядок, нуль або «хибу».
null — це окреме значення, яке означає «значення немає». Порожній рядок "" означає «рядок є, але він порожній». Нуль 0 означає «значення є і воно дорівнює нулю». false означає «логічне значення є і воно хибне». Якщо змішувати ці стани, контракт стає туманним: клієнту доводиться вгадувати, що саме ви хотіли сказати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ