Холодной весенней ночью я наконец-таки перешел на 33 уровень. Потирая свои программистские ручки, я уже готовился объять всю сферу сериализации, десериализации JSON, но, к сожалению, ничего не понял. Текст лекции не запомнился, а задачи решались как-то интуитивно. В связи с этим решил полезть в дебри Jackson Framework и разобраться, что же такое этот JSON.
Все свои познания постараюсь изложить практико-ориентированно и лаконично в формате шпаргалки (как для себя, так и для читателей).

Путешествие в Jackson Annotations
Первое, что мы встречаем на пути в JSON — это аннотация @JsonAutoDetect. На первый взгляд легкая аннотация, но с ней автору пришлось разбираться дольше всего. Аннотация имеет 5 нужных нам методов:
fieldVisibility() - сериализует поля только с указанным модификатором доступа
getterVisibility()/setterVisibility() - сериализует поля, у которых геттер/сеттер имеет указанный модификатор доступа
isGetterVisibility() - отдельная реализация для булевских геттеров
Важно понимать, что методы работают дизъюнктивно. Т.е. если поле соответствует хотя бы одному из указанных в аннотации параметров, то оно попадет в JSON.
Попытайтесь ответить, что выведет данный код, если мы создадим экземляр, используя конструктор без параметров:
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
setterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
isGetterVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC)
public class HeadClass {
public String name;
private Map<String, String> properties;
public Queue<String> queue;
protected List<String> list;
private int age;
private int number;
private boolean isHead;
protected HeadClass(int age) {
this.age = age;
}
public HeadClass() {}
Map<String, String> getProperties() {
return properties;
}
protected boolean isHead() {
return isHead;
}
protected void setProperties(Map<String, String> properties) {
this.properties = properties;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
А если убрать isGetterVisibility?
Четыре перечисленных метода конфигурировали процесс сериализации. Пятый же в свою очередь регулирует процесс десериализации:
creatorVisibility() - самый сложный метод для понимания. Он работает с конструкторами и с фабричными методами (методы, которые создают объект при обращении к ним). Рассмотрим пример:
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC)
public class HeadClass {
public String name;
public int id;
HeadClass(@JsonProperty(value = "name") String name, @JsonProperty(value = "id") int id) {
this.name = name;
this.id = id;
}
protected HeadClass(String name) {
this.name = name;
this.id = 123;
}
protected HeadClass(int id) {
this.id = id;
this.name = "Yes!";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
String forDeserialize = "{\"name\":\"No!\",\"id\":123}";
StringReader reader = new StringReader(forDeserialize);
HeadClass headClass1 = (HeadClass) mapper.readValue(reader, HeadClass.class);
}
Важное замечание по механизму десериализации! Когда мы пытаемся создать объект из JSON, то будет осуществляться поиск конструктора требуемого класса с таким же набором параметров, что и в JSON объекте. В примере выше наш JSON объект состоит из двух параметров: name, id. Угадайте, к какому конструктору он обратится. И да, если мы скомпилируем этот код, он выдаст ошибку, а почему? Потому что мы ограничили видимость конструктора (видны только конструкторы с модификатором protected, public). Если вы удалите creatorVisibility, то он заработает.
Возникает вопрос. А что за @JsonProperty в конструкторе. Об этом думаю рассказать в следующей части статьи.
@JsonProperty - маппинг между Java и JSON
Собственно, вот мы и добрались до одной из самых полезных аннотаций в Jackson. @JsonProperty позволяет нам явно указать, какое имя поля в JSON соответствует полю в Java-классе. Это особенно удобно, когда имена не совпадают или когда мы хотим обеспечить обратную совместимость.
Посмотрим на простой пример:
public class User {
@JsonProperty("user_name")
private String name;
@JsonProperty("user_age")
private int age;
// геттеры и сеттеры
}
При сериализации объекта User получим JSON вида:
{
"user_name": "Иван",
"user_age": 25
}
А что, если API возвращает поле то с одним именем, то с другим? Бывает такое в жизни, поверьте мне. Тут нас спасет свойство value с массивом альтернативных имен:
@JsonProperty(value = "name", alternate = {"username", "user_name", "fullName"})
private String name;
При десериализации Jackson будет искать в JSON любое из указанных имен.
@JsonIgnore и @JsonIgnoreProperties - скрываем лишнее
Иногда нам нужно скрыть какие-то поля от сериализации. Например, пароли, служебную информацию или просто данные, которые не должны попасть в API.
public class User {
private String name;
@JsonIgnore
private String password;
@JsonIgnore
private Date lastLoginDate;
// остальные поля
}
А если полей много и проще указать, что игнорировать на уровне класса:
@JsonIgnoreProperties({"password", "internalId", "debugInfo"})
public class User {
private String name;
private String password;
private long internalId;
private String debugInfo;
// остальные поля
}
Есть еще полезная штука - @JsonIgnoreProperties(ignoreUnknown = true). Она спасает, когда в JSON приходят поля, которых нет в нашем классе. Без этой аннотации Jackson выбросит исключение.
@JsonFormat - форматируем даты и числа
С датами в JSON всегда проблемы. То они приходят в виде строки, то как timestamp, то в каком-то экзотическом формате. @JsonFormat решает эту проблему:
public class Event {
@JsonFormat(pattern = "dd-MM-yyyy HH:mm:ss")
private Date eventDate;
@JsonFormat(pattern = "dd.MM.yyyy")
private LocalDate startDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.##")
private double price;
}
Кстати, часто приходится работать с timezone. Тогда пишем так:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Europe/Kiev")
private Date createdAt;
Собираем все вместе - практический пример
Давайте теперь создадим что-то более-менее реальное. Представим, что мы разрабатываем API для интернет-магазина:
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Product {
@JsonProperty("product_id")
private Long id;
@JsonProperty(value = "name", alternate = {"title", "product_name"})
private String name;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.##")
private BigDecimal price;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate createdDate;
@JsonIgnore
private String internalNotes;
private List<String> categories;
// конструктор для Jackson
public Product() {}
public Product(@JsonProperty("product_id") Long id,
@JsonProperty("name") String name) {
this.id = id;
this.name = name;
this.createdDate = LocalDate.now();
}
// геттеры и сеттеры...
}
Этот класс умеет работать с JSON вида:
{
"product_id": 12345,
"title": "Awesome Product",
"price": "99.99",
"createdDate": "2023-12-01",
"categories": ["electronics", "gadgets"],
"some_unknown_field": "will be ignored"
}
Полезные трюки, которые экономят время
За время работы с Jackson накопилось несколько лайфхаков:
1. Глобальная конфигурация ObjectMapper. Вместо аннотаций на каждом классе можно настроить поведение глобально:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
2. @JsonInclude - контролируем, что попадает в JSON:
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Response {
private String message;
private String error; // не попадет в JSON, если null
}
3. @JsonCreator для десериализации в immutable объекты:
public class ImmutableUser {
private final String name;
private final int age;
@JsonCreator
public ImmutableUser(@JsonProperty("name") String name,
@JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
}
Что дальше?
Jackson — это такой монстр, что сразу и не разберёшься, но возможностей у него вагон. Мы пока поковыряли базовые аннотации, которые решают процентов 80 задач на практике. В следующий раз хочу замахнуться на кастомные сериализаторы, полиморфизм и как это всё прикрутить к Spring Framework — будет огонь!
А пока мой совет: просто берите и кодьте. Замутите пару классов, поиграйтесь с разными аннотациями, попробуйте запихнуть даты в разные форматы. Я сам так в своё время знакомился с Jackson — писал, ломал, чинил, пока не стало ясно, как оно работает.
И помните - если что-то не работает, первым делом проверьте, есть ли у вас геттеры/сеттеры или конструктор по умолчанию. Это самые частые источники проблем для новичков.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ