Jackson
Jackson преобразует JSON и Java-модель. У него три уровня API:
- Streaming (
JsonParser,JsonGenerator) — минимальная память и ручной контроль tokens; - Tree model (
JsonNode) — изменяемое дерево для динамической структуры; - Data binding (
ObjectMapper) — преобразование POJO/record по типам и annotations.
Базовый пример
public record OrderDto(long id, String status) {}
ObjectMapper mapper = JsonMapper.builder()
.findAndAddModules()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
String json = mapper.writeValueAsString(new OrderDto(42, "PAID"));
OrderDto order = mapper.readValue(json, OrderDto.class);ObjectMapper дорог в создании и содержит caches. Обычно на приложение настраиваю один instance и затем переиспользую. После начала параллельного использования не меняю его конфигурацию.
Generics и стирание типов
Так тип элемента теряется:
List<?> values = mapper.readValue(json, List.class);Нужно передать полный тип:
List<OrderDto> values = mapper.readValue(
json,
new TypeReference<List<OrderDto>>() {}
);Эволюция контракта
При чтении внешнего API неизвестное поле часто допустимо, поэтому client может отключить FAIL_ON_UNKNOWN_PROPERTIES или точечно применить annotation. Но опечатка в собственном строгом config тогда тоже останется незамеченной. Политику выбираю по границе контракта, а не глобально “для удобства”.
Имена JSON-полей — часть внешнего контракта:
public record UserDto(
@JsonProperty("user_id") long id,
String name
) {}Переименование Java field не должно случайно ломать wire format.
Даты, null и отсутствующее поле
Для Java Time подключается соответствующий module. Формат и timezone фиксируются контрактом. В PATCH нужно различать:
- field отсутствует — не менять;
- field равен
null— очистить; - field содержит значение — установить.
Обычный nullable DTO не всегда выражает эти три состояния.
Опасные графы
Двунаправленные ORM-связи могут дать рекурсию parent → children → parent, lazy queries или утечку внутренних полей. В web-контракте использую отдельный DTO/projection, а не сериализую entity.
Полиморфная десериализация с автоматическим выбором класса опасна для недоверенного input. Разрешаю только явный закрытый набор типов и не включаю широкую default typing без необходимости.
Streaming
Для очень большого массива data binding создаёт весь object graph. Streaming читает по одному token/object и ограничивает память, но требует ручной валидации и закрытия parser.
Типичные сбои
- Создавать новый
ObjectMapperна каждый request. - Читать
List.classи получитьLinkedHashMapвместо DTO. - Сериализовать Hibernate entity с lazy graph.
- Путать отсутствующее поле и
nullв partial update. - Не ограничить polymorphic types для внешнего JSON.
- Принять локальную timezone JVM за часть API-контракта.
Вопросы для собеседования
Когда выбирать streaming API? Для большого/потокового JSON или минимального allocation.
Почему нужен TypeReference? Он сохраняет описание generic type для runtime-десериализации.
Можно ли разделять ObjectMapper между threads? Да, после завершённой конфигурации; перенастраивать используемый instance не стоит.
Почему entity не лучший response DTO? Контракт связывается с persistence model, возможны lazy queries, циклы и раскрытие полей.
Чем отсутствующее поле отличается от null? В partial update первое часто означает “не менять”, второе — “очистить”.
См. REST, Обобщения generics, Анализ производительности ORM систем.