JavaRush /Курсы /Java Server /JSON как строка не подходит:

JSON как строка не подходит: Jackson 3

Java Server
16 уровень , 0 лекция
Открыта

1. Сейчас: JSON всё ещё String

Если посмотреть на текущую версию ReadLater Starter после transport-рефакторинга, то картина довольно честная: запросы уходят, ответы возвращаются, статус можно проверить, JSON можно вывести. Но с точки зрения приложения данные всё ещё выглядят как «мешок символов». Это нормально на старте — ровно так большинство людей и начинает. Проблема начинается тогда, когда вы хотите не просто посмотреть на JSON, а использовать его.

В реальности transport-слой приносит нам примерно такое (упрощённо):


import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// HTTP-клиент — низкоуровневый транспорт: он пока ничего не знает про «книги», «авторов» и т.д.
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
        // Важно: здесь мы оперируем только URL и HTTP-методом — это ещё не «модель приложения»
        .uri(URI.create("https://catalog.example/api/search?q=clean%20code"))
        .GET()
        .build();

// Пока результат — это просто статус + строка (raw JSON)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

// Проверяем транспортный результат
System.out.println(response.statusCode()); // 200
// И печатаем тело как есть (это ещё не «данные приложения», а упаковка)
System.out.println(response.body());       // {"docs":[...],"numFound":123}

С точки зрения “мы умеем ходить по HTTP” — это победа. Но с точки зрения “мы строим приложение” у нас сразу возникает бытовой вопрос: а как теперь получить список книг, их названия и авторов так, чтобы код был читаемым и не ломался от первого чиха провайдера?

Можно, конечно, продолжать жить в режиме “ну я где-то видел, что там есть поле docs, сейчас его substring-ом вытащу”. И вот тут начинается то, что программисты называют “история успеха, но в жанре хоррор”.

2. Raw JSON-строка: «песок в шестерёнках»

Работать с JSON как со строкой — это как пытаться готовить борщ, не имея ножа, и вместо ножа использовать банковскую карту. Технически можно, но где-то после третьей морковки вы начнёте задумываться о смысле жизни, а не о рецепте. Строковые методы (contains, indexOf, substring) не понимают структуру JSON: они видят только символы, а не поля, массивы и вложенность.

Самый невинный пример обычно выглядит так:


 // Raw JSON как строка (мы вообще не понимаем структуру, просто видим символы)
 String rawJson = """
     {"title":"Clean Code","author":"Robert C. Martin"}
     """;

 // Проверка уровня «в строке встречается подстрока»
 boolean hasTitle = rawJson.contains("\"title\"");
 System.out.println(hasTitle); // true

И вроде даже не стыдно. Но это проверка уровня “в тексте есть слово title”. Она не говорит нам, какое значение у title, не гарантирует, что это именно то поле, которое нам нужно, и вообще легко обманется чем угодно.

Следующий шаг новичка — вытащить значение руками:


String rawJson = """
    {"title":"Clean Code","author":"Robert C. Martin"}
    """;

// Ищем начало значения поля title (хрупко: зависит от формата, пробелов, порядка полей)
int start = rawJson.indexOf("\"title\":\"") + "\"title\":\"".length();
// Ищем ближайшую кавычку как «конец значения» (ломается на экранировании и кавычках внутри)
int end = rawJson.indexOf("\"", start);

// Вырезаем кусок строки: «парсинг», который на самом деле не парсит JSON
String title = rawJson.substring(start, end);
System.out.println(title); // Clean Code

Пока JSON идеально ровный, без экранирования, без пробелов, без вложенности и без повторяющихся ключей — оно даже будет работать. Но реальный внешний JSON почти сразу ломает этот “подход” по куче причин.

Если провайдер добавит пробелы или поменяет порядок полей, строковая логика может внезапно перестать находить нужный кусок. Если в значении появятся кавычки, переносы строк или какие-то спецсимволы, ваш indexOf начнёт “резать” не там. Если title окажется внутри массива объектов, а не в корне — вы начнёте писать парсер массивов. Спойлер: вы случайно напишете свой «Jackson», только хуже, медленнее и с багами. Это нормальный путь — но лучше пройти его в теории, а не в продакшене и не в курсовом проекте.

И самое неприятное: строковый парсинг делает ошибки неочевидными. Он часто не падает красиво, а “немножко неправильно” вытаскивает данные, и вы потом долго думаете, почему у книги автор внезапно равен "Martin","status":"PLANNED"... (да, такое бывает, если вы случайно попали substring-ом в неправильное место).

3. Mapping layer: JSON → DTO

Когда приложение растёт, в нём очень полезно иметь чёткие границы ответственности. Transport-слой уже стал такой границей: он ходит по HTTP и возвращает статус + raw body. Теперь нам нужен следующий “переходник”: слой, который превращает JSON-строку в Java-объект понятной формы. Это и есть JSON mapping layer — отдельный шаг между response.body() и вашим прикладным кодом.

Если изобразить идею схематично, то получится примерно так:

flowchart LR
    %% Идея: «строка JSON» живёт на границе, а внутри приложения — типы и поля
    A["HTTP response.body() String"] --> B["JSON mapping layer Jackson"]
    B --> C["Typed DTO Java record"]
    C --> D["Дальше код работает с полями и типами, а не со строкой"]

С практической точки зрения mapping layer даёт три очень приземлённые выгоды.

Первая выгода — вы начинаете работать с данными через поля и типы, а не через “магические” куски строки. book.title() читается как человеческий текст. substring(17, 42) читается как “я устал, босс”.

Вторая выгода — ошибки становятся локальными. Если JSON не совпадает с ожидаемой формой, проблема проявляется ровно на границе JSON → DTO, а не через 15 строк, когда вы где-то случайно получили null и упали в другом месте.

Третья выгода — вы укрепляете дисциплину: raw JSON остаётся на границе transport/mapping, а внутрь приложения не протекает. Это очень важная привычка до Spring: позже фреймворк многое автоматизирует, но сама идея границ от этого не исчезает.

4. Jackson 3: переводчик JSON ↔ Java

Чтобы не изобретать велосипед из макарон и пластилина, мы берём библиотеку, которая профессионально занимается переводом между JSON и Java-объектами. В нашем курсе это Jackson 3 — и мы используем его как единый baseline. Центральный класс для повседневной работы здесь — ObjectMapper, и в Jackson 3 он находится в пакете tools.jackson.databind.

Самая базовая магия (на самом деле не магия, а нормальный инструмент) выглядит так: мы объявляем DTO, а потом просим Jackson прочитать JSON в этот DTO.


import tools.jackson.databind.ObjectMapper;

public class JacksonMiniDemo {

    // DTO (контракт): в таком виде мы хотим видеть данные внутри приложения
    record BookDto(String title, String author) {}

    public static void main(String[] args) throws Exception {
        // Raw JSON пришёл как строка (например, из response.body())
        String rawJson = """
            {"title":"Clean Code","author":"Robert C. Martin"}
            """;

        // ObjectMapper — наш «переводчик» между JSON и Java-типами
        ObjectMapper mapper = new ObjectMapper();

        // Десериализация: JSON -> типизированный DTO
        BookDto book = mapper.readValue(rawJson, BookDto.class);

        // Теперь работаем с полями, а не с подстроками
        System.out.println(book.title()); // Clean Code
    }
}

Обратите внимание на важный психологический момент: в коде больше нет “поиска подстроки \"title\":\"”. Мы просто говорим: “вот контракт, вот DTO, переведи”.

Обратная операция тоже есть: тот же ObjectMapper умеет собрать JSON из Java-объекта. Но здесь нам важнее именно чтение. Как только response.body() быстро превращается в DTO, приложение перестаёт жить в мире substring и начинает работать с типами.

Сейчас достаточно схватить сам принцип: ObjectMapper — это граница между строковым JSON и типизированными данными приложения. Отдельная инженерная привычка — держать такой маппинг собранным в одном месте и не размазывать его по коду.

5. Baseline: Jackson 3 без Jackson 2

В интернете вы найдёте тонны примеров “как парсить JSON в Java”. Проблема в том, что значительная часть этих примеров написана под Jackson 2, а Jackson 3 — это не просто “следующая версия”, там действительно поменялась важная часть “точек входа”: Maven-координаты и Java-пакеты. В Jackson 3 основные пакеты переехали в пространство имён tools.jackson.*, чтобы Jackson 2 и Jackson 3 могли сосуществовать, не конфликтуя на classpath.

Вот короткая табличка, которая спасает от 80% боли новичков:

Что вы видите в примере Скорее всего это Что делать в нашем курсе
implementation("com.fasterxml.jackson.core:jackson-databind:...") Jackson 2 Не копировать “в лоб”
implementation("tools.jackson.core:jackson-databind:3.0.4") Jackson 3 Это наш baseline
import com.fasterxml.jackson.databind.ObjectMapper; Jackson 2 У нас так не скомпилируется
import tools.jackson.databind.ObjectMapper; Jackson 3 Так и нужно
import com.fasterxml.jackson.annotation.JsonProperty; Аннотации Jackson Это нормально даже в Jackson 3 (аннотации остались в старом groupId)

И вот здесь скрывается коварная ловушка. Когда студент видит ошибку импорта, он часто делает “логичный” ход: добавляет ещё одну зависимость, чтобы “починить импорт”. Например, в проекте уже есть Jackson 3, но вы добавили Jackson 2 “потому что статья так сказала”. В итоге получается две разные библиотеки, которые делают примерно одно и то же, но живут в разных пакетах. Да, именно поэтому Jackson 3 и переехал в tools.jackson.*: чтобы coexist был возможен.

Только coexist — это возможность для больших проектов, которые мигрируют постепенно. Для учебного проекта это обычно превращается в хаос: вы начинаете путаться, какой ObjectMapper вы используете, какие исключения ловить, какие зависимости реально нужны. Поэтому правило курса простое и слегка занудное (значит полезное): держим один baseline Jackson 3 и не делаем в проекте “зоопарк версий”.

Ещё одна деталь, которая выглядит как мелочь, но постоянно ломает жизнь: в Jackson 3 jackson-databind действительно зависит от jackson-annotations, и в POM есть прямое пояснение, что аннотации остаются в groupId Jackson 2 (com.fasterxml.jackson.core). То есть комбинация tools.jackson.databind.ObjectMapper + com.fasterxml.jackson.annotation.JsonProperty — это не ошибка, а нормальная часть задумки Jackson 3.

6. ReadLater Starter: от response.body() к DTO

Чтобы не воспринимать Jackson как “ещё одну тему в вакууме”, давайте привяжем всё к нашему проекту. У нас есть команда вида catalog search <query>, которая делает HTTP-вызов к внешнему каталогу. Transport-слой возвращает нам статус и JSON-строку. Следующий шаг — не “печатать строку красивее”, а превратить её в DTO, потому что именно DTO дальше будет нормализоваться и выводиться пользователю.

Упрощённо поток мыслей в коде должен стать таким:


import tools.jackson.databind.ObjectMapper;
import java.net.http.HttpResponse;

public class CatalogFlowSketch {

    // DTO успешного ответа провайдера (в реальности полей будет больше)
    record ProviderSearchResponseDto(int numFound) {}

    public ProviderSearchResponseDto parse(HttpResponse<String> response) throws Exception {
        // Сначала — транспортная проверка: статус, ошибки, ретраи и т.д.
        if (response.statusCode() != 200) {
            // В этом месте мы явно фиксируем, что проблема не в JSON-маппинге, а в ответе провайдера
            throw new IllegalStateException("Provider returned status " + response.statusCode());
        }

        // Потом — маппинг: превращаем JSON-строку в тип
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(response.body(), ProviderSearchResponseDto.class);
    }
}

Здесь важна сама граница: после проверки статуса JSON должен быстро превратиться в DTO, а не поехать строкой дальше по проекту. Отдельная инженерная привычка — не создавать ObjectMapper заново в каждом месте и не размазывать mapping-логику по коду. Но даже в таком коротком скетче уже видно главное: transport-успех и JSON-mapping — это два разных шага, и путать их не стоит.

7. Типичные ошибки

В начале работы с Jackson почти все ошибки происходят не потому, что “Jackson сложный”, а потому что мозг по инерции тянет вас обратно в мир строк и “быстрых решений”. Это нормально: вы учитесь. Главное — вовремя поймать привычку за руку и мягко, но настойчиво вернуть её в цивилизацию.

Ошибка №1: продолжать парсить JSON строковыми методами “потому что так быстрее”.
Обычно это начинается с contains, потом появляется substring, потом “маленькая регулярка”, а потом внезапно вы пишете обработку экранирования кавычек и понимаете, что вы только что начали разрабатывать библиотеку уровня Jackson, но без зарплаты. Правильный переход на этом дне — воспринимать строку как транспортную упаковку, которую нужно распаковать в DTO и выкинуть.

Ошибка №2: пытаться маппить body до проверки statusCode.
ObjectMapper не обязан “угадывать”, что вы получили. Если провайдер вернул 404 или 500 с другим JSON, а вы пытаетесь прочитать это как успешный DTO, вы получите ошибку десериализации в месте, где на самом деле проблема transport-уровня. Дисциплина простая: сначала ветка по статусу, потом JSON → DTO.

Ошибка №3: копировать примеры Jackson 2 и “чинить” их добавлением новых зависимостей.
Если в статье ObjectMapper импортируется из com.fasterxml.jackson.databind, а у вас в проекте Jackson 3, не нужно спасать ситуацию добавлением Jackson 2. У нас baseline — Jackson 3, и ObjectMapper должен быть из tools.jackson.databind. Аннотации при этом могут оставаться com.fasterxml.jackson.annotation.* — это ожидаемо.

Ошибка №4: протаскивать raw JSON через несколько слоёв “на всякий случай”.
Иногда кажется, что строку полезно оставить “на потом”: вдруг пригодится. На практике это приводит к тому, что половина кода начинает зависеть от формата провайдера, а любые изменения внешнего контракта разлетаются по всему проекту. Гораздо спокойнее держать raw JSON только на границе transport/mapping, а внутрь отдавать DTO.

1
Задача
Java Server, 16 уровень, 0 лекция
Недоступна
Jackson 3 вместо ручного разбора JSON
Jackson 3 вместо ручного разбора JSON
1
Задача
Java Server, 16 уровень, 0 лекция
Недоступна
Отдельный mapping-класс для одной JSON-строки
Отдельный mapping-класс для одной JSON-строки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ