JavaRush /Курсы /Spring Boot /Форма JSON для CourseCard<...

Форма JSON для CourseCard

Spring Boot
12 уровень, 3 лекция
Открыта

1. Форма JSON как контракт

Когда вы только стартуете, хочется радоваться любому JSON: «ура, оно не упало, значит всё хорошо». Но у backend-сервиса есть неприятная особенность: как только кто-то начинает потреблять ваш ответ (браузер, фронтенд, другой сервис, даже вы сами через неделю) — форма JSON становится контрактом. И контракт ломается не по настроению, а по факту.

Точечная настройка одного типа ещё не равна удачному публичному контракту. Клиенту нужен не просто “какой-то JSON”, а стабильная форма CourseCard, которую можно парсить без угадываний и сюрпризов.

Представьте, что ваш сервис — это кафе, а JSON — меню. Пока посетитель один (вы в Postman) — можно хоть “Суп №1”, хоть “Супчик вкусненький”. Но как только появляется второй посетитель (условный фронтенд), внезапно выясняется, что «Супчик вкусненький» — плохое имя для позиции: сегодня вы так назвали борщ, завтра — уху, а клиент уже привык, что это борщ. Вот ровно так же ломаются клиенты от «декоративного JSON».

В рамках нашего курса мы не уходим в полноценный REST API design, но мы обязаны сделать базовую вещь: выбрать JSON-форму CourseCard, которая одновременно:

  1. читаемая человеком,
  2. стабильно машиночитаемая,
  3. предсказуемая по всему приложению.

Чтобы это сделать, мы разберём три больных места для начинающего: enum, даты и деньги.

2. CourseCard в catalog-service: поля и форма

Сейчас у нас учебный read-only сервис, и мы можем позволить себе «простую жизнь»: возвращать наружу модель CourseCard без отдельного DTO-слоя. Это не универсальный рецепт на все времена, но для Boot-курса это отличный компромисс: меньше кода, меньше маппинга, больше понимания того, что делает Spring Boot и Jackson.

Базовая форма ответа

Чтобы не расплываться в абстракции, зафиксируем ориентир. Пример одной карточки курса в JSON (примерно так будет выглядеть GET /api/catalog/courses/{slug}):

{
  "id": "spring-boot",
  "slug": "spring-boot",
  "title": "Spring Boot",
  "shortDescription": "Boot fundamentals",
  "track": "SPRING",
  "level": "BASIC",
  "durationDays": 28,
  "featured": true,
  "published": true,
  "launchDate": "2026-04-01",
  "price": {
    "amount": 19900,
    "currency": "GBP"
  }
}

Здесь специально всё «скучно». И это комплимент. Скучный JSON — обычно хороший JSON: его легко парсить, легко сравнивать, легко тестировать, легко объяснить.

Мы видим три ключевые зоны, которые легко испортить: track/level как enum, launchDate как дата, price как money-like объект.

Имена полей в JSON и Java

Самый частый вопрос от новичка звучит примерно так: «А почему JSON-ключи называются shortDescription и durationDays, а не short_description и duration_days?». Ответ простой: по умолчанию Jackson берёт имена Java-свойств (геттеров/компонентов record) и превращает их в JSON-ключи.

То есть если в Java у вас поле/геттер называется getLaunchDate(), то в JSON вы увидите ключ launchDate. И пока мы не вводим специальные naming strategies, @JsonProperty и прочие «тонкие настройки», это самая предсказуемая и понятная линия поведения.

Здесь важно не перепутать: в YAML-конфигурации мы позже будем часто использовать kebab-case (launch-date, duration-days) из-за relaxed binding и читабельности конфигов. Но JSON-ответы веб-слоя — это отдельная история, и в текущем baseline мы держим camelCase, потому что он естественно вытекает из Java-кода.

Мини-таблица: Java-типы в JSON

Небольшая «карта местности», чтобы вы каждый раз не гадали:

Java-тип в модели Как выглядит в JSON Пример Комментарий
String строка
"spring-boot"
Всё просто
int, long число
28
Без кавычек
boolean логическое
true
Не "true"
enum строка
"SPRING"
Обычно Enum.name()
LocalDate строка
"2026-04-01"
ISO-8601, если настроено адекватно
Money объект
{ "amount": 19900, "currency": "GBP" }
Сохраняем структуру

Эта таблица — как «легенда» к карте. Она не заменяет понимание, но помогает не удивляться каждой мелочи.

3. enum в JSON: стабильные значения

С enum у начинающих регулярно случается два крайних состояния. Первое: «ну и ладно, пусть как-то сериализуется». Второе: «хочу сделать красиво: “Spring-направление”, “уровень: базовый”, и желательно на русском, и чтобы смайлик в конце». Второе состояние обычно заканчивается тем, что кто-то плачет, и это не обязательно вы.

Простой baseline: enum как строка

Если у вас есть:

public enum CourseTrack {
    // Важно: значения enum становятся частью JSON-контракта (обычно через Enum.name()).
    // Поэтому это должны быть стабильные технические коды, а не «красивые подписи».
    SPRING,
    JAVA_BACKEND,
    DATA,
    INFRA,
    ADVANCED
}

то Jackson по умолчанию сделает JSON вроде "SPRING".

То же самое для уровня:

public enum CourseLevel {
    // Эти значения также «уезжают наружу» в JSON, поэтому переименование = изменение контракта.
    BASIC,
    JUNIOR_PLUS,
    MIDDLE
}

Это очень «скучный» и при этом очень стабильный подход. Он хорошо работает для учебного сервиса и для большинства реальных внутренних API. Плюс такой JSON легко использовать и в фильтрах, и в UI, и в тестах.

Главный подводный камень: переименование констант

Тут важно сказать честно и прямо: enum в JSON сериализуется в строку, которая обычно равна имени константы. Значит, если вы переименовали JUNIOR_PLUS в JUNIORPLUS «потому что так красивее» — вы поменяли JSON. А если JSON поменялся, клиенту больно.

Это не значит «никогда не переименовывайте enum». Это значит «относитесь к этому как к изменению контракта». Даже в учебном проекте полезно сформировать правильную привычку: enum — не просто “список вариантов в Java”, это ещё и значения, которые увидит внешний мир.

Человекочитаемость enum и JSON

Очень заманчиво сделать вместо "SPRING" что-то вроде "Spring-направление", потому что человеку приятнее. Но в backend обычно выигрывает другое: JSON — для данных, а не для оформления.

Человекочитаемость нужна UI. А UI почти всегда хочет:

  • иметь возможность локализовать подписи (русский/английский),
  • иметь возможность менять текст без смены контракта,
  • иметь возможность сортировать/фильтровать по стабильному значению.

Если вы запихнёте «красивую подпись» прямо в JSON, вы фактически смешаете «данные» и «интерфейс». На маленьком проекте это незаметно, на большом — это делает изменения мучительными.

4. Даты: единый формат LocalDate

Даты — это классическая тема, где программисты делятся на две категории. Первая: «ну это же просто строка». Вторая: «давайте устроим битву за временные зоны до последнего студента». Мы выберем третий путь: разумный baseline без религиозных войн.

launchDate в домене и выбор LocalDate

В доменной модели каталога launchDate — это дата запуска курса, без времени суток. Нам не важно «в 14:37 по Токио» он стартует или «в 09:00 по Нью-Йорку». Это не расписание вебинара, а дата релиза/старта.

Именно поэтому в Java это очень хорошо выражается типом LocalDate. Он буквально означает «дата без времени и без часового пояса». Это снижает количество ошибок: вы не сможете случайно “прибавить два часа” и внезапно получить вчерашний день из-за зоны.

LocalDate в JSON

В адекватном baseline LocalDate сериализуется в строку ISO-8601 формата:

"launchDate": "2026-04-01"

Почему это круто:

  • формат короткий и стандартный;
  • он сортируется как строка правильно (лексикографически "2026-04-02" больше "2026-04-01" — магия, которая на самом деле просто удачный формат);
  • его легко распарсить в любом языке.

Почему это ещё и практично: вы не заставляете клиента гадать, это Unix timestamp или «миллисекунды с эпохи», и не провоцируете ошибки на ровном месте.

Антипример: «декоративная дата» как строка

Вот такой подход выглядит дружелюбно:

"launchDate": "1 April 2026"

Но это JSON, который сразу создаёт проблемы. Клиенту нужно угадать язык, формат, регистр месяца, а потом ещё и понять, что делать, если вы внезапно захотите "April 1, 2026" (потому что «так принято в США») или "01.04.2026" (потому что «так принято у нас»).

Когда вы отдаёте данные, вы не хотите, чтобы клиент парсил «вкусные строки». Вы хотите, чтобы клиент парсил структурированные значения. Поэтому ISO-строка "2026-04-01" — скучно, предсказуемо, правильно.

Почему пока не трогаем время и таймзоны

Можно сказать так: как только вы добавляете время и часовой пояс, вы добавляете в проект ещё один маленький курс «Как не сломать время». В этом курсе (Spring Boot как платформа) мы держим фокус и выбираем самые простые устойчивые сущности.

Если однажды в домене появится «время старта вебинара», тогда да, можно будет обсуждать Instant, OffsetDateTime, нормальные таймзоны, и так далее. Но CourseCard сейчас — это карточка каталога, а не календарь космических запусков.

5. Money: форма и структура

Money — это тот самый тип, на котором люди любят “срезать углы”, а потом обнаруживают, что углы были несущими. Можно сделать цену строкой, можно числом, можно объектом… и внезапно выяснить, что разные варианты влияют на то, насколько JSON остаётся машиночитаемым и расширяемым.

Три популярные формы Money в JSON

Обычно встречаются три стратегии:

Вариант Пример JSON Плюсы Минусы
Только число
"price": 19900
просто непонятная валюта, нельзя расширять без ломки
Строка-лейбл
"price": "19900 GBP"
красиво глазу трудно парсить, теряется структура, UI-логика лезет в API
Объект
"price": { "amount": 19900, "currency": "GBP" }
машиночитаемо, расширяемо чуть больше символов

В учебном catalog-service мы выбираем объект. Потому что наша цель — не «самый короткий JSON в мире», а понятный и стабильный baseline.

Рекомендованная модель Money в Java

Если вы используете обычный класс, он может выглядеть так (в пакете catalog.domain):

public class Money {
    // Сумма в минимальных «логических» единицах, которые вы выбрали в проекте (в учебном — просто целое).
    private final int amount;
    // Код валюты, который будет уходить в JSON как строка (например, "GBP").
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public int getAmount() { return amount; }
    public String getCurrency() { return currency; }
}

Jackson увидит геттеры и сделает:

"price": { "amount": 19900, "currency": "GBP" }

Если у вас Money оформлен record’ом, Jackson тоже справится. Важно не то, class это или record, а то, что это структурированное значение, а не «текст для красоты».

Нюанс: что такое amount

В реальной жизни деньги — сложная тема, и на уровне production принято хранить суммы в минимальных единицах (центы, копейки) либо использовать BigDecimal + фиксированные правила округления. Но наш курс — не про финтех, и мы не строим платёжную систему.

Поэтому в учебном проекте мы делаем максимально простую вещь: amount — целое число, а currency — строковый код. Главное, что мы выигрываем, — это понятная структура, а не «идеальная денежная математика».

Когда уместна строка через @JacksonComponent

После знакомства с type-specific кастомизацией легко появляется соблазн: «О! Давайте всегда сериализовать Money как строку "19900 GBP" — ведь красиво!». И иногда это правда удобно, например, для маленьких внутренних логов или очень узких сценариев, где деньги никогда не будут фильтроваться/сортироваться и нужны только как подпись.

Но для публичной формы CourseCard обычно лучше сохранить объект. И причина простая: сегодня вы хотите "19900 GBP", а завтра UI попросит отдельно подсветить валюту, посчитать скидку или перевести в другую валюту. Если цена уже пришла структурой, это делается легко. Если пришла строкой, клиент начинает писать «парсинг денег регулярками». А регулярки для денег — это как молоток для микросхем: что-то точно будет сломано.

В нашем baseline мы оставляем Money объектом, а @JacksonComponent для него рассматриваем только как осознанное исключение под отдельную задачу, но не как default snapshot CourseCard.

6. Пример формы JSON для CourseCard

Сейчас соберём всё в одну понятную картину. Нам важно увидеть, что итоговая форма JSON — это не «настройка где-то в облаках», а прямое следствие Java-модели: enum превращаются в строки, LocalDate превращается в ISO-строку, Money превращается во вложенный объект.

Минимальная модель CourseCard (record)

Если вам удобно использовать records для неизменяемых моделей, CourseCard может выглядеть так:

import java.time.LocalDate;

public record CourseCard(
        // Технический идентификатор (часто совпадает со slug, но не обязан).
        String id,
        // То, что обычно используется в URL.
        String slug,
        // Человекочитаемый заголовок.
        String title,
        // Enum уезжает в JSON как стабильная строка (Enum.name()).
        CourseTrack track,
        // Enum уезжает в JSON как стабильная строка (Enum.name()).
        CourseLevel level,
        // Дата без времени и таймзоны: в JSON ожидаем ISO-строку вида YYYY-MM-DD.
        LocalDate launchDate,
        // Деньги как структурированный объект: amount + currency.
        Money price
) {}

Это коротко, читаемо и отлично подходит для read-only модели. Если у вас CourseCard сделан обычным классом с геттерами — JSON будет выглядеть почти так же, так что суть не меняется.

Быстрый «снимок формы» через JsonMapper

Иногда полезно проверить форму JSON без браузера и без HTTP — просто сериализовать объект и увидеть строку. Например, внутри небольшого runner’а:

import tools.jackson.databind.json.JsonMapper;
import java.time.LocalDate;

public class JsonShapeDemo {

    public static void main(String[] args) throws Exception {
        // Создаём mapper вручную, чтобы увидеть форму JSON без Spring MVC и без HTTP.
        // В самом Boot-приложении mapper уже приходит из auto-configuration.
        JsonMapper mapper = new JsonMapper();

        // Собираем пример доменного объекта: это данные, которые Jackson будет превращать в JSON-структуру.
        CourseCard card = new CourseCard(
                "spring-boot", "spring-boot", "Spring Boot",
                CourseTrack.SPRING, CourseLevel.BASIC,
                LocalDate.of(2026, 4, 1),
                new Money(19900, "GBP")
        );

        // Сериализация: никаких конкатенаций строк — только данные -> JSON.
        System.out.println(mapper.writeValueAsString(card));
        // {"id":"spring-boot","slug":"spring-boot","title":"Spring Boot","track":"SPRING","level":"BASIC","launchDate":"2026-04-01","price":{"amount":19900,"currency":"GBP"}}
    }
}

Здесь важна не сама main(), а наблюдение: вы буквально видите, как Java-структура становится JSON-структурой. Никаких «ручных строк», никаких конкатенаций. Только данные → JSON.

Да, в реальном Spring Boot приложении JsonMapper уже создаётся Boot’ом и приходит как bean, и у него будут ваши глобальные настройки spring.jackson.*. Но как демонстрация формы это упражнение очень наглядное.

Форма на endpoint’ах: один объект и список объектов

Когда controller возвращает один CourseCard, клиент получает JSON-объект. Когда controller возвращает List<CourseCard>, клиент получает JSON-массив объектов.

Смысловая форма карточки при этом не меняется. Это важное правило согласованности: одна и та же сущность должна одинаково выглядеть и в списке, и в детальном просмотре.

Пример массива (упрощённо):

[
  {
    "slug": "spring-boot",
    "track": "SPRING",
    "launchDate": "2026-04-01",
    "price": { "amount": 19900, "currency": "GBP" }
  },
  {
    "slug": "spring-data-jpa",
    "track": "DATA",
    "launchDate": "2026-05-15",
    "price": { "amount": 24900, "currency": "GBP" }
  }
]

Видите, насколько удобно читать и парсить такой JSON: типы не угадываются, они очевидны.

7. Типичные ошибки при выборе формы JSON для CourseCard

Ошибка №1: превращать структуру в “красивые строки” слишком рано.
Очень легко скатиться в формат "price": "19 900 GBP" и "launchDate": "1 April 2026", потому что так приятно глазу. Но вы платите за это потерей структуры: клиенту нужно парсить строки обратно в числа и даты, а это всегда источник багов. JSON для данных должен быть скучным и предсказуемым, а «красивость» — задача интерфейса.

Ошибка №2: разные форматы дат на разных endpoint’ах.
Если где-то дата "2026-04-01", а где-то внезапно 1711929600 (секунды эпохи) или "01.04.2026", клиент начинает жить в мире “угадай формат”. Даже один такой выброс делает весь сервис менее надёжным. Выберите один формат (в нашем baseline — ISO-строка) и держитесь его.

Ошибка №3: “улучшать” enum-значения так, что они становятся нестабильными.
Сегодня вам кажется, что "SPRING" — сухо, и хочется "Spring Framework Track". Завтра маркетинг попросит "Spring направление". Послезавтра — "Spring 💚". И вот уже API меняется от настроения. enum в JSON должен быть стабильным техническим значением, а подписи должны жить отдельно.

Ошибка №4: смешивать в голове YAML-конфиг и JSON-ответы.
У нас в проекте YAML и правда чаще будет в kebab-case, а JSON — в camelCase. Новички иногда пытаются «сделать одинаково везде» и начинают менять JSON naming strategy без реальной необходимости. В итоге получается дополнительная настройка, дополнительная сложность и дополнительные сюрпризы. На базовом уровне лучше принять: конфиг и web-ответы — разные слои, у них разные удобные форматы.

Ошибка №5: использовать double для денег, а потом удивляться странным значениям.
Если вы когда-нибудь увидите цену 19.8999999997 там, где ожидали 19.9, знайте: это не «Spring снова колдует», это обычная математика double. В нашем проекте мы держим amount целым числом, чтобы не устраивать “фестиваль плавающей точки” в учебном сервисе.

1
Задача
Spring Boot, 12 уровень, 3 лекция
Недоступна
Форма JSON для одной карточки курса
Форма JSON для одной карточки курса
1
Задача
Spring Boot, 12 уровень, 3 лекция
Недоступна
Одинаковая JSON-форма для списка карточек курса
Одинаковая JSON-форма для списка карточек курса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ