Большая часть веба на сегодняшний день обменивается данными в формате JSON. Веб-серверы, веб-приложения и мобильные приложения, даже устройства IoT общаются друг с другом, используя JSON. Простой и гибкий способ обработки JSON необходим любому программному обеспечению, чтобы выжить в современном мире.

Эта статья сопровождается примером рабочего кода на GitHub.

Что такое JSON?

JSON (от англ JavaScript Object Notation) — это текстовый формат для представления структурированных данных на основе синтаксиса объектов JavaScript. Благодаря своему гибкому и простому формату он стал чрезвычайно популярным. По сути, он следует модели карты «ключ-значение», допускающей вложенные объекты и массивы:

{
  "array": [
    1,
    2,
    3
  ],
  "boolean": true,
  "color": "gold",
  "null": null,
  "number": 123,
  "object": {
    "a": "b",
    "c": "d"
  },
  "string": "Hello World"
}

Что такое Jackson?

Jackson в основном известен как библиотека, которая конвертирует строки JSON и простые объекты Java (англ POJO — Plain Old Java Object). Он также поддерживает многие другие форматы данных, такие как CSV, YML и XML.

Многие предпочитают Jackson благодаря его зрелости (он существует уже 13 лет) и отличной интеграции с популярными фреймворками, такими как Spring. Более того, это проект с открытым исходным кодом, который активно развивается и поддерживается широким сообществом.

Под капотом у Jackson есть три основных пакета: Streaming, Databind и Annotations. При этом Jackson предлагает нам три способа обработки преобразования JSON-POJO:

Потоковое API

Это самый быстрый подход из трех и с наименьшими накладными расходами. Он читает и записывает содержимое JSON в виде дискретных событий. API предоставляет JsonParser, который считывает JSON в POJO, и JsonGenerator, который записывает POJO в JSON.

Модель дерева

Модель дерева создает в памяти древовидное представление документа JSON. ObjectMapper отвечает за построение дерева из узлов JsonNode. Это наиболее гибкий подход, поскольку он позволяет перемещаться по дереву узлов, когда документ JSON не соответствует в достаточной мере POJO.

Привязка данных

Это позволяет нам выполнять преобразование между документами POJO и JSON с помощью средств доступа к свойствам или с помощью аннотаций. Он предлагает два типа привязки:

  • Простая привязка данных, которая преобразует JSON в Java Maps, Lists, Strings, Numbers, Booleans, null объекты и обратно.

  • Полная привязка данных, которая преобразует JSON в любой класс Java и из него.

ObjectMapper

ObjectMapper — наиболее часто используемая часть библиотеки Jackson, так как является самым простым способом преобразования между POJO и JSON. Она находится в com.fasterxml.jackson.databind.

Метод readValue() используется для преобразования (десериализации) JSON из строки, потока или файла в POJO.

С другой стороны, метод writeValue() используется для преобразования POJO в JSON (сериализация).

Способ, которым ObjectMapper определяет, какое поле JSON соответствует какому полю POJO, заключается в сопоставлении имен полей JSON с именами геттеров и сеттеров в POJO.

Это делается путем удаления частей «get» и «set» в именах геттеров и сеттеров и преобразования первого символа имени оставшегося метода в нижний регистр.

Например, предположим, у нас есть поле JSON с именем name: ObjectMapper сопоставит его с геттером getName() и сеттером setName() в POJO.

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

Зависимости Maven

Прежде чем мы посмотрим на код, нам нужно добавить зависимость Jackson Maven jackson-databind, которая, в свою очередь, транзитивно добавляет jackson-annotations и jackson-core.

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

Мы также используем Lombok для обработки шаблонного кода для геттеров, сеттеров и конструкторов.

Базовая сериализация и десериализация JSON с Jackson

Давайте рассмотрим наиболее важные варианты использования Jackson с примерами кода.

Базовое преобразование POJO/JSON с использованием ObjectMapper

Давайте начнем с представления простого POJO под названием Employee:

@Getter  
@AllArgsConstructor  
@NoArgsConstructor  
public class Employee {  
    private String firstName;  
    private String lastName;  
    private int age;  
}

Начнем с преобразования POJO в строку JSON:

public class JacksonTest {  
  
  ObjectMapper objectMapper = new ObjectMapper();
  
  @Test  
  void pojoToJsonString() throws JsonProcessingException {  
        Employee employee = new Employee("Mark", "James", 20);  
  
        String json = objectMapper.writeValueAsString(employee);  
  
        System.out.println(json);  
    }  
}

В качестве вывода увидим следующее:

{"firstName":"Mark","lastName":"James","age":20}

Теперь посмотрим, как преобразовать строку JSON в объект Employee с помощью ObjectMapper.

public class JacksonTest {  
  ...
  @Test  
  void jsonStringToPojo() throws JsonProcessingException {  
        String employeeJson = "{\n" +  
                " \"firstName\" : \"Jalil\",\n" +  
                " \"lastName\" : \"Jarjanazy\",\n" +  
                " \"age\" : 30\n" +  
                "}";  
  
        Employee employee = objectMapper.readValue(employeeJson, Employee.class);  
  
        assertThat(employee.getFirstName()).isEqualTo("Jalil");  
    }  
}

ObjectMapper также предлагает богатый API для чтения JSON из разных источников в разные форматы, давайте проверим самые важные из них.

Создание POJO из файла JSON

Это делается с помощью метода readValue().

Файл JSON в тестовых ресурсах employee.json:

{  
  "firstName":"Homer",  
  "lastName":"Simpson",  
  "age":44  
}
public class JacksonTest {
	...
	@Test  
	void jsonFileToPojo() throws IOException {  
	    File file = new File("src/test/resources/employee.json");  
	  
	    Employee employee = objectMapper.readValue(file, Employee.class);  
	  
	    assertThat(employee.getAge()).isEqualTo(44);  
	    assertThat(employee.getLastName()).isEqualTo("Simpson");  
	    assertThat(employee.getFirstName()).isEqualTo("Homer");  
	}
}

Создание POJO из массива байт в формате JSON

public class JacksonTest {
	...
	@Test  
	void byteArrayToPojo() throws IOException {  
	    String employeeJson = "{\n" +  
	            " \"firstName\" : \"Jalil\",\n" +  
	            " \"lastName\" : \"Jarjanazy\",\n" +  
	            " \"age\" : 30\n" +  
	            "}";  
	  
	    Employee employee = objectMapper.readValue(employeeJson.getBytes(), Employee.class);  
	  
	    assertThat(employee.getFirstName()).isEqualTo("Jalil");  
	}
}

Создание списка POJO из JSON

Иногда документ JSON представляет собой не объект, а список объектов. Давайте посмотрим, как можно его прочитать.

employeeList.json:

[  
  {  
    "firstName":"Marge",  
    "lastName":"Simpson",  
    "age":33  
  },  
  {  
    "firstName":"Homer",  
    "lastName":"Simpson",  
    "age":44  
  }  
]
public class JacksonTest {
	...
	@Test 
	void fileToListOfPojos() throws IOException {  
	    File file = new File("src/test/resources/employeeList.json");  
	    List<Employee> employeeList = objectMapper.readValue(file, new TypeReference<>(){});  
	  
	    assertThat(employeeList).hasSize(2);  
	    assertThat(employeeList.get(0).getAge()).isEqualTo(33);  
	    assertThat(employeeList.get(0).getLastName()).isEqualTo("Simpson");  
	    assertThat(employeeList.get(0).getFirstName()).isEqualTo("Marge");  
	}
}

Создание Map из JSON

Мы можем преобразовать JSON в Java Map, что очень удобно, если мы не знаем, чего ожидать от файла JSON, который мы пытаемся спарсить. ObjectMapper превратит имя каждой переменной в JSON в ключ для Map, а значение этой переменной — в значение по этому ключу.

public class JacksonTest {
	...
	@Test  
	void fileToMap() throws IOException {  
	    File file = new File("src/test/resources/employee.json");  
	    Map<String, Object> employee = objectMapper.readValue(file, new TypeReference<>(){});  
	  
	    assertThat(employee.keySet()).containsExactly("firstName", "lastName", "age");  
	  
	    assertThat(employee.get("firstName")).isEqualTo("Homer");  
	    assertThat(employee.get("lastName")).isEqualTo("Simpson");  
	    assertThat(employee.get("age")).isEqualTo(44);  
	}
}

Игнорирование неизвестных полей JSON

Иногда ожидаемый нами JSON может иметь дополнительные поля, не определенные в POJO. Поведение Jackson по умолчанию заключается в том, чтобы в таких случаях генерировать исключение UnrecognizedPropertyException. Однако же мы можем настроить Jackson так, чтобы он не расстраивался по поводу неизвестных полей и просто игнорировал их. Это делается путем установки FAIL_ON_UNKNOWN_PROPERTIES ObjectMapper в false.

employeeWithUnknownProperties.json:

{  
  "firstName":"Homer",  
  "lastName":"Simpson",  
  "age":44,  
  "department": "IT"  
}
public class JacksonTest {
	...
	@Test  
	void fileToPojoWithUnknownProperties() throws IOException {  
	    File file = new File("src/test/resources/employeeWithUnknownProperties.json");  
  	    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);  
	  
	    Employee employee = objectMapper.readValue(file, Employee.class);  
	  
	    assertThat(employee.getFirstName()).isEqualTo("Homer");  
	    assertThat(employee.getLastName()).isEqualTo("Simpson");  
	    assertThat(employee.getAge()).isEqualTo(44);  
	}
}

Работа с датами in Jackson

Преобразование дат может быть непростым занятием, поскольку они могут быть представлены во многих форматах и уровнях спецификации (секунды, миллисекунды и т. д.).

Дата в JSON

Прежде чем говорить о преобразовании дат и Jackson, нам нужно поговорить о новом Date API в Java 8. Он был введен для устранения недостатков более старых java.util.Date и java.util.Calendar. В основном нас интересует использование класса LocalDate, который предлагает эффективный способ представления даты и времени.

Для этого нам нужно добавить в Jackson дополнительный модуль, чтобы он мог обрабатывать LocalDate.

<dependency>  
    <groupId>com.fasterxml.jackson.datatype</groupId>  
    <artifactId>jackson-datatype-jsr310</artifactId>  
    <version>2.13.3</version>  
</dependency>

Затем нам нужно сказать ObjectMapper найти и зарегистрировать новый модуль, который мы только что добавили.

public class JacksonTest {
	ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
	...
	@Test  
	void orderToJson() throws JsonProcessingException {  
	    Order order = new Order(1, LocalDate.of(1900,2,1));  
	  
	    String json = objectMapper.writeValueAsString(order);  
	  
	    System.out.println(json);  
	}
}

В этом случае поведение Jackson по умолчанию состоит в том, чтобы показывать дату как [гггг-ММ-дд]. Таким образом, вывод будет {"id":1,"date":[1900,2,1]}

Однако мы можем указать Jackson, в каком формате нам нужна дата. Это можно сделать с помощью аннотации @JsonFormat.

public class Order {  
    private int id;  
    @JsonFormat(pattern = "dd/MM/yyyy")  
    private LocalDate date;  
}
@Test  
void orderToJsonWithDate() throws JsonProcessingException {  
    Order order = new Order(1, LocalDate.of(2023, 1, 1));  
  
    String json = objectMapper.writeValueAsString(order);  
  
    System.out.println(json);  
}

Это должно вывести {"id":1,"date":"01/01/2023"}.

JSON в дату

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

order.json:

{  
  "id" : 1,  
  "date" : "30/04/2000"  
}
public class JacksonTest {
	...
	@Test  
	void fileToOrder() throws IOException {  
	    File file = new File("src/test/resources/order.json");  
	  
	    Order order = objectMapper.readValue(file, Order.class);  
	  
	    assertThat(order.getDate().getYear()).isEqualTo(2000);  
	    assertThat(order.getDate().getMonthValue()).isEqualTo(4);  
	    assertThat(order.getDate().getDayOfMonth()).isEqualTo(30);  
	}
}

Аннотации Jackson 

Важную роль в настройке процесса преобразования JSON/POJO играют аннотации. Мы видели пример с преобразованием даты, где мы использовали аннотацию @JsonFormat. Аннотации влияют на то, как данные читаются, записываются или даже на то и другое. Давайте рассмотрим некоторые из этих аннотаций на основе их категорий.

Аннотации чтения

Они влияют на то, как Jackson преобразует JSON в POJO.

@JsonSetter

Это полезно, когда мы хотим сопоставить поле в строке JSON с полем в POJO, где их имена не совпадают.

@NoArgsConstructor  
@AllArgsConstructor  
@Getter  
public class Car {  
    @JsonSetter("carBrand")  
    private String brand;  
}
{  
  "carBrand" : "BMW"  
}
public class JacksonTest {
	...
	@Test  
	void fileToCar() throws IOException {  
	    File file = new File("src/test/resources/car.json");  
	  
	    Car car = objectMapper.readValue(file, Car.class);  
	  
	    assertThat(car.getBrand()).isEqualTo("BMW");  
	}
}

@JsonAnySetter

Эта аннотация полезна в случаях, когда JSON содержит некоторые поля, не объявленные в POJO. Он используется с сеттером, который вызывается для каждого нераспознанного поля.

public class Car {  
    @JsonSetter("carBrand")  
    private String brand;  
    private Map<String, String> unrecognizedFields = new HashMap<>();  
  
    @JsonAnySetter  
    public void allSetter(String fieldName, String fieldValue) {  
        unrecognizedFields.put(fieldName, fieldValue);  
    }  
}

файл carUnrecognized.json:

{  
  "carBrand" : "BMW",  
  "productionYear": 1996  
}
public class JacksonTest {
	...
	@Test  
	void fileToUnrecognizedCar() throws IOException {  
	    File file = new File("src/test/resources/carUnrecognized.json");  
	  
	    Car car = objectMapper.readValue(file, Car.class);  
	  
	    assertThat(car.getUnrecognizedFields()).containsKey("productionYear");  
	}
}

Аннотации записи

Они влияют на то, как Jackson преобразует POJO в JSON.

@JsonGetter

Это полезно, когда мы хотим сопоставить поле POJO с полем JSON, используя другое имя. Например, предположим, что у нас есть класс Cat с полем name, но мы хотим, чтобы его JSON-имя было catName.

@NoArgsConstructor  
@AllArgsConstructor  
public class Cat {  
    private String name;  
  
    @JsonGetter("catName")  
    public String getName() {  
        return name;  
    }  
}
public class JacksonTest {
	...
	@Test  
	void catToJson() throws JsonProcessingException {  
	    Cat cat = new Cat("Monica");  
	  
	    String json = objectMapper.writeValueAsString(cat);  
	  
	    System.out.println(json);  
	}
}

Это выведет

{
	"catName":"Monica"
}

@JsonAnyGetter

Эта аннотация позволяет нам использовать объект Map как источник свойств JSON. Скажем, у нас есть эта карта как поле в классе Cat.

@NoArgsConstructor  
@AllArgsConstructor  
public class Cat {  
      private String name;  
  
	  @JsonAnyGetter  
	  Map<String, String> map = Map.of(  
	            "name", "Jack",  
	  "surname", "wolfskin"  
	  );
  ...
  }
@Test  
void catToJsonWithMap() throws JsonProcessingException {  
    Cat cat = new Cat("Monica");  
  
   String json = objectMapper.writeValueAsString(cat);  
  
   System.out.println(json);  
}

Вывод будет следующим:

{
  "catName":"Monica",
  "name":"Jack",
  "surname":"wolfskin"
}

Аннотации чтения и записи

Эти аннотации влияют как на чтение, так и на запись JSON.

@JsonIgnore

Поле с аннотацией игнорируется как при записи, так и при чтении JSON.

@AllArgsConstructor  
@NoArgsConstructor  
@Getter  
public class Dog {  
    private String name;  
    @JsonIgnore  
	private int age;  
}
public class JacksonTest {
	...
	@Test  
	void dogToJson() throws JsonProcessingException {  
	    Dog dog = new Dog("Max", 3);  
	  
	    String json = objectMapper.writeValueAsString(dog);  
	  
	    System.out.println(json);  
	}
}

Это выведет {"name":"Max"}

То же самое относится и к чтению в POJO.

Предположим, у нас есть файл dog.json:

{  
  "name" : "bobby",  
  "age" : 5  
}
public  class  JacksonTest  { 
	 ...
	@Test  
	void fileToDog() throws IOException {  
	    File file = new File("src/test/resources/dog.json");  
	  
	    Dog dog = objectMapper.readValue(file, Dog.class);  
	  
	    assertThat(dog.getName()).isEqualTo("bobby");  
	    assertThat(dog.getAge()).isNull();  
	}
}

У Jackson есть еще много полезных аннотаций, которые дают нам больше контроля над процессом сериализации/десериализации. Полный их список можно найти в репозитории Jackson на Github.

Резюме

  • Jackson — одна из самых мощных и популярных библиотек для обработки JSON в Java.

  • Jackson включает три основных модуля: Streaming API, Tree Model и Data Binding.

  • Jackson предоставляет ObjectMapper, который легко настраивается в соответствии с потребностями, с возможностью задавать его свойства и использовать аннотации.

Все примеры кода лежат в репозитории на GitHub.


Приглашаем всех желающих на открытый урок «Реляционные базы данных для начинающих Java-разработчиков». На уроке поговорим о месте реляционных баз данных в архитектуре информационных систем. Рассмотрим основные компоненты и возможности РСУБД на примере PostgreSQL. Сделаем обзор основных технологий по работе с реляционными БД в Java (JDBC, JPA/Hibernate, Spring Data и др.). Регистрируйтесь по ссылке.

Комментарии (0)