JavaRush /Курси /Java Server /Прості JSON-значення

Прості JSON-значення

Java Server
Рівень 10 , Лекція 1
Відкрита

1. JSON-документ = JSON-значення

Коли людина вперше бачить JSON, вона часто запам’ятовує його як «щось у фігурних дужках». Це нормальна стартова ілюзія, приблизно як думати, що інтернет — це «сторінки в браузері». Насправді JSON набагато простіший і водночас суворіший: JSON-документ — це одне JSON-значення. Так, інколи це значення виглядає як об’єкт {...} або масив [...], але «атом» JSON — саме значення, і значення має тип.

У JSON існує всього шість видів значень: рядок, число, булеве значення, null, об’єкт і масив. Сьогодні ми свідомо беремо лише «прості» (скалярні) типи: рядок, число, boolean і null. Вони трапляються буквально в кожному API: title і author — рядки, id і count — числа, isAvailableboolean, а 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 або fboolean. Якщо nnull. Якщо { — об’єкт. Якщо [ — масив.

Можна навіть зробити маленьку «пам’ятку» у вигляді таблиці:

Перший символ після пробілів Тип значення
" рядок
0…9 або
-
число
t /
f
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 означає «логічне значення є і воно хибне». Якщо змішувати ці стани, контракт стає туманним: клієнту доводиться вгадувати, що саме ви хотіли сказати.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ