Jackson — это популярная библиотека для сериализации/десериализации Java-объектов в различные текстовые форматы. Основной функционал для работы с форматом JSON — класс ObjectMapper. А работать с другими форматами помогут его наследники (XmlMapper, YAMLMapper). Благодаря наследованию работа со всеми форматами будет происходить единообразно, через единый интерфейс.

Качаем jar-ники

Перед тем, как изучить примеры, нужно скачать jar-файлы Jackson-а и подключить их к проекту в IntellijIDEA. Рассмотрим, как искать нужные файлы на примере jackson-databind:

  1. Перейди на сайт Maven Repository.

  2. В поисковую строку введи “jackson-databind”, получишь результат:

  3. Нас интересует первый результат поиска, переходим по ссылке.

  4. Иногда может потребоваться конкретная версия библиотеки, чтобы обеспечить совместимость с другими компонентами проекта. Тебе подойдет последняя версия (на момент написания лекции это 2.13.2.2), переходи по ссылке.

  5. На открывшейся странице тебе нужна ссылка “bundle”:

  6. Скачиваем jar-файл по ссылке.

Аналогично можно найти и скачать остальные необходимые jar-ники:

Скачав все необходимые файлы, подключи их к проекту в IntellijIDEA:

  1. Открой настройки проекта (это можно сделать комбинацией Ctrl+Alt+Shift+S).

  2. Перейди в Libraries.

  3. Жми +, затем Java, выбери все скачанные файлы. Должно получиться так:

  4. На этом подготовка закончена, будем пробовать ObjectMapper в деле.

Сериализация в JSON

Сначала сериализуем какой-нибудь объект в JSON:


import com.fasterxml.jackson.databind.ObjectMapper;
 
class Book {
	public String title;
	public String author;
	public int pages;
}
 
public class Solution {
	public static void main(String[] args) throws Exception {
    	Book book = new Book();
    	book.title = "Обитаемый остров";
    	book.author = "Стругацкий А., Стругацкий Б.";
    	book.pages = 413;
 
    	ObjectMapper mapper = new ObjectMapper();
    	String jsonBook = mapper.writeValueAsString(book);
    	System.out.println(jsonBook);
	}
}

Запустив main, получишь такой вывод:

{"title":"Обитаемый остров","author":"Стругацкий А., Стругацкий Б.","pages":413}

У ObjectMapper-а есть много дополнительных настроек. Воспользуемся одной из них, чтобы JSON-строка была удобочитаемо отформатирована. После создания объекта ObjectMapper выполним команду:

mapper.enable(SerializationFeature.INDENT_OUTPUT);

Информация в выводе осталась та же, но добавились отступы и переносы строк:

{
  "title" : "Обитаемый остров",
  "author" : "Стругацкий А., Стругацкий Б.",
 "pages" : 413
}

Десериализация из JSON

Теперь выполним обратное действие: десериализуем строку в объект. Чтобы можно было оценить работу программы, в класс Book добавим метод toString:


@Override
public String toString() {
	return "Book{" +
        	"title='" + title + '\'' +
        	", author='" + author + '\'' +
        	", pages=" + pages +
        	'}';
}

И выполним такой main:


public static void main(String[] args) throws Exception {
	String jsonString = "{\"title\":\"Обитаемый остров\",\"author\":\"Стругацкий А., Стругацкий Б.\",\"pages\":413}";
	Book book = new ObjectMapper().readValue(jsonString, Book.class);
	System.out.println(book);
}

Вывод:

Book{title='Обитаемый остров', author='Стругацкий А., Стругацкий Б.', pages=413}

Метод readValue перегружен, у него есть много вариаций, принимающих файл, ссылку, различные потоки чтения и т. д. Для упрощения в нашем примере использован вариант, принимающий JSON в виде строки.

Как уже говорилось выше, ObjectMapper имеет много настроек, рассмотрим некоторые из них.

Игнорируем неизвестные свойства

Рассмотрим ситуацию, когда у JSON-строки есть свойство, которого нет в классе Book:


public static void main(String[] args) throws Exception {
	String jsonString = """
        	{
          	"title" : "Обитаемый остров",
          	"author" : "Стругацкий А., Стругацкий Б.",
          	"pages" : 413,
          	"unknown property" : 42
        	}""";
	ObjectMapper mapper = new ObjectMapper();
	Book book = mapper.readValue(jsonString, Book.class);
	System.out.println(book);
}

Попытавшись выполнить этот код, получим UnrecognizedPropertyException. Такое поведение установлено по умолчанию, но мы можем его изменить:


ObjectMapper mapper =
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

При создании объекта ObjectMapper используем метод configure, чтобы установить нужную настройку в false. Метод configure изменяет объект, у которого его вызвали, и возвращает этот же объект, поэтому его можно вызывать и по-другому:


ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

По функциональности такая запись аналогична предыдущей.

Если теперь запустить main, десериализация пройдет успешно, а unknown property будет проигнорировано.

Удобные аннотации

Jackson предоставляет нам ряд аннотаций для всесторонней кастомизации сериализации. Рассмотрим несколько самых полезных:

@JsonIgnore — ставится над элементом, который нужно игнорировать при сериализации/десериализации:


class Book {
	public String title;
	@JsonIgnore
	public String author;
	public int pages;
}

В результате при сериализации поле author не попадет в результирующий JSON. При десериализации поле author получит значение по умолчанию (null), даже если в JSON-е было другое значение.

@JsonFormat — позволяет задать формат сериализованных данных. Добавим в класс Book еще одно поле типа Date:


class Book {
	public String title;
	public String author;
	public int pages;
	public Date createdDate = new Date();
}

После сериализации получим такой JSON:

 {
  "title" : "Обитаемый остров",
  "author" : "Стругацкий А., Стругацкий Б.",
  "pages" : 413,
  "createdDate" : 1649330880788
}

Как видишь, дата сериализировалась в виде числа. Добавим аннотацию и зададим формат:


class Book {
	public String title;
	public String author;
	public int pages;
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
	public Date createdDate = new Date();
}

Теперь результат сериализации:

{
  "title" : "Обитаемый остров",
  "author" : "Стругацкий А., Стругацкий Б.",
  "pages" : 413,
  "createdDate" : "2022-04-07"
}

@JsonProperty — позволяет изменить имя свойства, в которое будет сериализировано поле. Еще этой аннотацией можно помечать методы, и их возвращаемое значение будет преобразовано в JSON-свойство при сериализации:


class Book {
	@JsonProperty("name")
	public String title;
	public String author;
	public int pages;
 
	@JsonProperty("quotedTitle")
	public String getQuotedTitle() {
    	    return "«" + title + "»";
	}
}

Результат сериализации:

{
  "author" : "Стругацкий А., Стругацкий Б.",
  "pages" : 413,
  "name" : "Обитаемый остров",
  "quotedTitle" : "«Обитаемый остров»"
}

@JsonInclude — с помощью этой аннотации можно указать, в каком случае поле должно сериализироваться. Можно добавлять как отдельным полям, так и всему классу. Сначала попробуем сериализовать объект с неинициализированными полями:


public class Solution {
	public static void main(String[] args) throws Exception {
    		Book book = new Book();

    		ObjectMapper mapper = new ObjectMapper();
    		mapper.enable(SerializationFeature.INDENT_OUTPUT);
    		String jsonBook = mapper.writeValueAsString(book);
    		System.out.println(jsonBook);
	}
}

Результат сериализации:

{
  "title" : null,
  "author" : null,
  "pages" : 0
}

А если добавить аннотацию:


@JsonInclude(JsonInclude.Include.NON_NULL)
class Book {
	public String title;
	public String author;
	public int pages;
}

То получим результат:

{
  "pages" : 0
}

Теперь поля, имеющие значение null, не сериализовались.

@JsonPropertyOrder — позволяет задать порядок сериализации полей:


@JsonPropertyOrder({"author", "title", "pages"})
class Book {
	public String title;
	public String author;
	public int pages;
}

Результат сериализации:

{
  "author" : "Стругацкий А., Стругацкий Б.",
  "title" : "Обитаемый остров",
  "pages" : 413
}

Сейчас можешь просто запомнить, как использовать аннотации, а в конце этого модуля будем более детально с ними знакомиться и создавать собственные аннотации.

Сериализация и десериализация в XML

Если нужно сериализовать в XML-формат, можем использовать все те же настройки и аннотации. Единственным отличием будет реализация объекта mapper:


public static void main(String[] args) throws Exception {
	Book book = new Book();
	book.title = "Обитаемый остров";
	book.author = "Стругацкий А., Стругацкий Б.";
	book.pages = 413;
 
	ObjectMapper mapper = new XmlMapper();
	mapper.enable(SerializationFeature.INDENT_OUTPUT);
	String xmlBook = mapper.writeValueAsString(book);
	System.out.println(xmlBook);
}

Вывод:

 <Book>
  <title>Обитаемый остров</title>
  <author>Стругацкий А., Стругацкий Б.</author>
  <pages>413</pages>
</Book>

Десериализация XML:


public static void main(String[] args) throws Exception {
   String xmlString = """
            <Book>
             <title>Обитаемый остров</title>
             <author>Стругацкий А., Стругацкий Б.</author>
             <pages>413</pages>
           </Book>""";
   ObjectMapper mapper = new XmlMapper();
   Book book = mapper.readValue(xmlString, Book.class);
   System.out.println(book);
}

Сериализация и десериализация в YAML

Аналогично XML действуем и с YAML:


public static void main(String[] args) throws Exception {
	Book book = new Book();
	book.title = "Обитаемый остров";
	book.author = "Стругацкий А., Стругацкий Б.";
	book.pages = 413;
 
	ObjectMapper mapper = new YAMLMapper();
	mapper.enable(SerializationFeature.INDENT_OUTPUT);
	String yamlBook = mapper.writeValueAsString(book);
	System.out.println(yamlBook);
}

Вывод:

---
title: "Обитаемый остров"
author: "Стругацкий А., Стругацкий Б."
pages: 413

Десериализация YAML:


public static void main(String[] args) throws Exception {
   String yamlString = """
           ---
           title: "Обитаемый остров"
           author: "Стругацкий А., Стругацкий Б."
           pages: 413""";
   ObjectMapper mapper = new YAMLMapper();
   Book book = mapper.readValue(yamlString, Book.class);
   System.out.println(book);
}