Статьи

Введение в JSON с Java

Программирование играет незаменимую роль в разработке программного обеспечения, но, хотя оно эффективно для систематического решения проблем, оно не дотягивает до нескольких важных категорий. В облачной и распределенной эпохе программного обеспечения данные играют ключевую роль в получении дохода и поддержании эффективного продукта. Независимо от того, обмениваются ли данные конфигурации или данные через веб-интерфейс прикладного программирования (API), наличие компактного, понятного человеку языка данных имеет важное значение для поддержания динамической системы и позволяет специалистам, не занимающимся программированием, понять систему.

Хотя двоичные данные могут быть очень сжатыми, их невозможно прочитать без надлежащего редактора или обширных знаний. Вместо этого большинство систем используют текстовый язык данных, такой как eXtensive Markup Language (XML), Another Another Markup Language (YML) или Нотация объектов Javascript (JSON). Хотя XML и YML имеют свои собственные преимущества, JSON стал лидером в области API-интерфейсов конфигурации и передачи представительного состояния (REST). Он сочетает в себе простоту и достаточно богатство, чтобы легко выразить широкий спектр данных.

Хотя JSON достаточно прост для чтения и записи как программистами, так и непрограммистами, наши программисты должны быть способны производить и использовать JSON, что может быть далеко не просто. К счастью, существует множество библиотек Java JSON, которые эффективно решают эту задачу. В этой статье мы расскажем об основах JSON, в том числе о базовом обзоре и общих случаях использования, а также о том, как сериализовать объекты Java в JSON и как десериализовать объекты JSON в объекты Java с помощью почтенной библиотеки Джексона. Обратите внимание, что весь код, используемый для этого урока, можно найти на Github .

Основы JSON

JSON не начинался как текстовый язык конфигурации. Строго говоря, это фактически код Javascript, который представляет поля объекта и может использоваться компилятором Javascript для воссоздания объекта. На самом элементарном уровне объект JSON представляет собой набор пар ключ-значение, заключенный в фигурные скобки и двоеточие, отделяющее ключ от значения; ключ в этой паре — строка, заключенная в кавычки, а значение — значение JSON. Значение JSON может быть любым из следующих:

  • другой объект JSON
  • строка
  • число
  • логический литерал true 
  • логический литерал false 
  • буквальный null 
  • массив значений JSON

Например, простой объект JSON с только строковыми значениями может быть:

{"firstName": "John", "lastName": "Doe"}

Аналогичным образом, другой объект JSON может выступать в качестве значения, а также числа, либо логического литерала, либо  null литерала, как показано в следующем примере:

{
  "firstName": "John", 
  "lastName": "Doe",
  "alias": null,
  "age": 29,
  "isMale": true,
  "address": {
    "street": "100 Elm Way",
    "city": "Foo City",
    "state": "NJ",
    "zipCode": "01234"
  }
}

Массив JSON — это список значений, разделенных запятыми, в квадратных скобках. Этот список не содержит ключей; вместо этого он содержит только любое количество значений, включая ноль. Массив, содержащий нулевые значения, называется пустым массивом . Например:

{
  "firstName": "John",
  "lastName": "Doe",
  "cars": [
    {"make": "Ford", "model": "F150", "year": 2018},
    {"make": "Subaru", "model": "BRZ", "year": 2016},
  ],
  "children": []
}

Массивы JSON могут быть разнородными и содержать значения смешанных типов, таких как: 

["hi", {"name": "John Doe"}, null, 125]

Тем не менее, это строго соблюдаемое общее соглашение для массивов, содержащих одинаковые типы данных. Аналогично, объекты в массиве обычно имеют те же ключи и типы значений, что и в предыдущем примере. Например, каждое значение в  cars массиве имеет ключ  make со строковым значением,  key модель со строковым значением и ключ  year с числовым значением.

Важно отметить, что порядок пар ключ-значение в объекте JSON и порядок элементов в массиве JSON не учитывают равенство двух объектов или массивов JSON соответственно. Например, следующие два объекта JSON эквивалентны:

{"firstName": "John", "lastName": "Doe"}
{"lastName": "Doe", "firstName": "John"}

JSON Files

Файл, содержащий данные JSON, использует  .json расширение файла и имеет корень либо объекта JSON, либо массива JSON. Файлы JSON могут содержать не более одного корневого объекта. Если требуется более одного объекта, корнем файла JSON должен быть массив, содержащий нужные объекты. Например, оба из следующих допустимых файлов JSON:

[
  {"name": "John Doe"},
  {"name": "Jane Doe"}
]
{
  "name": "John Doe",
  "age": 29
}

Несмотря на то, что для файла JSON допустимо иметь массив в качестве корня, обычной практикой для корня является объект с одним ключом, содержащим массив. Это делает назначение массива явным:

{
  "accounts": [
    {"name": "John Doe"},
    {"name": "Jane Doe"}
  ]
}

Преимущества JSON

JSON имеет три основных преимущества: (1) он может быть легко прочитан людьми, (2) он может быть эффективно проанализирован с помощью кода, и (3) он выразителен. Первое преимущество очевидно при чтении файла JSON. Хотя некоторые файлы могут стать большими и сложными — содержащими много разных объектов и массивов, — размер этих файлов громоздок, а не язык, на котором они написаны. Например, приведенные выше фрагменты могут быть легко прочитаны без использования контекста или использования определенного набора инструментов.

Аналогично, код также может относительно легко анализировать JSON; весь язык JSON может быть сведен к набору простых конечных автоматов, как описано в стандарте обмена данными JSON ECMA-404 . В следующих разделах мы увидим, что эта простота преобразуется в библиотеки синтаксического анализа JSON на Java, которые могут быстро и точно использовать строки или файлы JSON и создавать пользовательские объекты Java.

Наконец, простота интерпретации не умаляет ее выразительности. Практически любая объектная структура может быть рекурсивно сведена к объекту или массиву JSON. Те поля, которые трудно сериализовать в объекты JSON, могут быть преобразованы в строку, и их строковое значение будет использовано вместо представления их объекта. Например, если мы хотим сохранить регулярное выражение в качестве значения, мы можем сделать это, просто превратив регулярное выражение в строку.

Понимая основы JSON, мы теперь можем углубиться в детали взаимодействия с фрагментами JSON и файлами через приложения Java.

Сериализация Java-объектов в JSON

Хотя процесс токенизации и анализа строк JSON утомителен и запутан, в Java существует множество библиотек, которые инкапсулируют эту логику в простые в использовании объекты и методы, которые делают взаимодействие с JSON почти тривиальным. Одна из самых популярных среди этих библиотек — FasterXML Jackson .

Джексон сосредотачивается вокруг простой концепции:  ObjectMapper. An  ObjectMapper — это объект, который инкапсулирует логику для взятия объекта Java и его сериализации в JSON, передачи JSON и десериализации его в указанный объект Java. В первом случае мы можем создавать объекты, представляющие нашу модель, и делегировать логику создания JSON  ObjectMapper. Следуя предыдущим примерам, мы можем создать два класса для представления нашей модели: человек и адрес:

public class Person {

    private String firstName;
    private String lastName;
    private String alias;
    private int age;
    private boolean isMale;
    private Address address;

    public Person(String firstName, String lastName, String alias, int age, boolean isMale, Address address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.alias = alias;
        this.age = age;
        this.isMale = isMale;
        this.address = address;
    }

    public Person() {}

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isMale() {
        return isMale;
    }

    public void setMale(boolean isMale) {
        this.isMale = isMale;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + firstName + ", lastName=" + lastName + ", alias=" + alias + ", age=" + age
                + ", isMale=" + isMale + ", address=" + address + "]";
    }
}

public class Address {

    private String street;
    private String city;
    private String state;
    private String zipCode;

    public Address(String street, String city, String state, String zipCode) {
        this.street = street;
        this.city = city;
        this.state = state;
        this.zipCode = zipCode;
    }

    public Address() {}

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "Address [street=" + street + ", city=" + city + ", state=" + state + ", zipCode=" + zipCode + "]";
    }
}

Person И  Address классы как Plain Old Java Objects (POJOs) и не содержат каких — либо JSON-специфический код. Вместо этого мы можем создать экземпляры этих классов в объектах и ​​передать созданные объекты  writeValueAsString методу  ObjectMapper. Этот метод создает объект JSON, представленный как  String предоставленный объект. Например:

Address johnDoeAddress = new Address("100 Elm Way", "Foo City", "NJ", "01234");
Person johnDoe = new Person("John", "Doe", null, 29, true, johnDoeAddress);

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(johnDoe);

System.out.println(json);

Выполняя этот код, мы получаем следующий вывод:

{"firstName":"John","lastName":"Doe","alias":null,"age":29,"address":{"street":"100 Elm Way","city":"Foo City","state":"NJ","zipCode":"01234"},"male":true}

Если мы красиво напечатаем (отформатируем) этот вывод JSON, используя  JSON Validator и Formatter , мы увидим, что он почти соответствует нашему первоначальному примеру:

{
   "firstName":"John",
   "lastName":"Doe",
   "alias":null,
   "age":29,
   "address":{
      "street":"100 Elm Way",
      "city":"Foo City",
      "state":"NJ",
      "zipCode":"01234"
   },
   "male":true
}

По умолчанию Джексон будет рекурсивно проходить по полям объекта, предоставленного  writeValueAsString методу, используя имя поля в качестве ключа и сериализуя значение в соответствующее значение JSON. В случае  String полей значение является строкой; для чисел (например, int) результат числовой; для  booleans результат — логическое значение; для объектов (таких как Address) этот процесс сериализации рекурсивно повторяется, в результате чего получается объект JSON.

Два заметных различия между нашим исходным JSON и JSON, выданным Джексоном, заключаются в том, что (1) порядок пар ключ-значение отличается и (2) male вместо ключа есть  ключ  isMale. В первом случае Джексон не обязательно выводит ключи JSON в том же порядке, в котором соответствующие поля появляются в объекте Java. Например,  male ключ является последним ключом в выведенном JSON, даже если  isMale поле находится перед  address полем в  Person объекте. Во втором случае по умолчанию  boolean флаги формы  isFooBar приводят к парам ключ-значение с ключом формы fooBar.

Если мы хотим, чтобы имя ключа было таким isMale, как оно было в нашем исходном примере JSON, мы можем аннотировать получатель для нужного поля, используя  JsonProperty аннотацию, и явно указывать имя ключа:

public class Person {

    // ...

    @JsonProperty("isMale") 
    public boolean isMale() {
        return isMale;
    }

    // ...
}

Повторный запуск приложения приводит к следующей строке JSON, которая соответствует желаемому результату:

{
   "firstName":"John",
   "lastName":"Doe",
   "alias":null,
   "age":29,
   "address":{
      "street":"100 Elm Way",
      "city":"Foo City",
      "state":"NJ",
      "zipCode":"01234"
   },
   "isMale":true
}

В случаях, когда мы хотим удалить  null значения из выходных данных JSON (где отсутствующий ключ неявно обозначает  null значение, а не явно включает  null значение), мы можем аннотировать затронутый класс следующим образом:

@JsonInclude(Include.NON_NULL)
public class Person {
    // ...
}

Это приводит к следующей строке JSON:

{
   "firstName":"John",
   "lastName":"Doe",
   "age":29,
   "address":{
      "street":"100 Elm Way",
      "city":"Foo City",
      "state":"NJ",
      "zipCode":"01234"
   },
   "isMale":true
}

В то время как полученная строка JSON эквивалентна строке JSON, которая явно включала  null  значение, вторая строка JSON экономит место. Эта эффективность более применима к более крупным объектам, которые имеют много  null полей. 

Сериализация коллекций

Чтобы создать массив JSON из объектов Java, мы можем передать  Collection объект (или любой подкласс) в  ObjectMapper. Во многих случаях List будет использоваться , так как он больше всего напоминает абстракцию массива JSON. Например, мы можем сериализовать  List из  Person объектов следующим образом :

Address johnDoeAddress = new Address("100 Elm Way", "Foo City", "NJ", "01234");
Person johnDoe = new Person("John", "Doe", null, 29, true, johnDoeAddress);

Address janeDoeAddress = new Address("200 Boxer Road", "Bar City", "NJ", "09876");
Person janeDoe = new Person("Jane", "Doe", null, 27, false, janeDoeAddress);

List<Person> people = List.of(johnDoe, janeDoe);

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(people);

System.out.println(json);

Выполнение этого кода приводит к следующему выводу:

[
   {
      "firstName":"John",
      "lastName":"Doe",
      "age":29,
      "address":{
         "street":"100 Elm Way",
         "city":"Foo City",
         "state":"NJ",
         "zipCode":"01234"
      },
      "isMale":true
   },
   {
      "firstName":"Jane",
      "lastName":"Doe",
      "age":27,
      "address":{
         "street":"200 Boxer Road",
         "city":"Bar City",
         "state":"NJ",
         "zipCode":"09876"
      },
      "isMale":false
   }
]

Запись сериализованных объектов в файл

Помимо сохранения полученного JSON в  String переменную, мы можем записать строку JSON непосредственно в выходной файл, используя  writeValue метод, предоставляя  File объект, который соответствует желаемому выходному файлу (например, john.json):

Address johnDoeAddress = new Address("100 Elm Way", "Foo City", "NJ", "01234");
Person johnDoe = new Person("John", "Doe", null, 29, true, johnDoeAddress);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File("john.json"), johnDoe);

Сериализованный Java-объект также может быть записан в любой  OutputStream, что позволяет выводить JSON-поток в нужное место (например, по сети или в удаленной файловой системе).

Десериализация JSON в объекты Java

С пониманием того, как взять существующие объекты и сериализовать их в JSON, также важно понять, как десериализовать существующий JSON в объекты. Этот процесс обычен при десериализации файлов конфигурации и напоминает его аналог сериализации, но он также отличается некоторыми важными способами. Во-первых, JSON по своей сути не сохраняет информацию о типе. Таким образом, глядя на фрагмент JSON, невозможно узнать тип представляемого объекта (без явного добавления дополнительного ключа, как в случае с  _class ключом в MongoDB).база данных). Чтобы правильно десериализовать фрагмент JSON, мы должны явно сообщить десериализатору ожидаемый тип. Во-вторых, ключи, присутствующие во фрагменте JSON, могут отсутствовать в десериализованном объекте. Например, если более новая версия объекта — содержащая новое поле — была сериализована в JSON, но JSON десериализуется в более старую версию объекта.

Чтобы решить первую проблему, мы должны явно сообщить  ObjectMapper требуемый тип десериализованного объекта. Например, мы можем предоставить строку JSON, представляющую  Person объект, и дать команду  ObjectMapper десериализации его в  Person объект:

String json = "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"alias\":\"Jay\","
    + "\"age\":29,\"address\":{\"street\":\"100 Elm Way\",\"city\":\"Foo City\","
    + "\"state\":\"NJ\",\"zipCode\":\"01234\"},\"isMale\":true}";

ObjectMapper mapper = new ObjectMapper();
Person johnDoe = mapper.readValue(json, Person.class);
System.out.println(johnDoe);

Выполнение этого фрагмента приводит к следующему выводу:

Person [firstName=John, lastName=Doe, alias=Jay, age=29, isMale=true, address=Address [street=100 Elm Way, city=Foo City, state=NJ, zipCode=01234]]

Важно отметить, что существует два требования, которые должен иметь десериализованный тип (или любые рекурсивно десериализованные типы):

  1. Класс должен иметь конструктор по умолчанию
  2. У класса должны быть сеттеры для каждого из полей

Кроме того,  isMale получатель должен быть украшен  JsonProperty аннотацией (добавленной в предыдущем разделе), обозначающей, что имя ключа для этого поля равно isMale. Удаление этой аннотации приведет к исключению во время десериализации, поскольку ObjectMapperпо умолчанию настроен сбой, если в десериализованном JSON найден ключ, который не соответствует полю в предоставленном типе. В частности, так как  ObjectMapper ожидает maleполе (если не сконфигурировано с JsonPropertyаннотацией), десериализация завершается неудачно, так как male в Person классе не найдено  поле  .

Чтобы исправить это, мы можем аннотировать  Person класс  JsonIgnoreProperties аннотацией. Например:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
    // ...
}

Затем мы можем добавить новое поле, например,  favoriteColor во фрагмент JSON, и десериализовать фрагмент JSON в  Person объект. Важно отметить, однако, что значение  favoriteColor ключа будет потеряно в десериализованном  Person объекте, поскольку нет поля для хранения значения:

String json = "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"alias\":\"Jay\","
    + "\"age\":29,\"address\":{\"street\":\"100 Elm Way\",\"city\":\"Foo City\","
    + "\"state\":\"NJ\",\"zipCode\":\"01234\"},\"isMale\":true, \"favoriteColor\":\"blue\"}";

ObjectMapper mapper = new ObjectMapper();
Person johnDoe = mapper.readValue(json, Person.class);
System.out.println(johnDoe);

Выполнение этого фрагмента приводит к следующему выводу:

Person [firstName=John, lastName=Doe, alias=Jay, age=29, isMale=true, address=Address [street=100 Elm Way, city=Foo City, state=NJ, zipCode=01234]]

Десериализация коллекций

В некоторых случаях JSON, который мы хотим десериализовать, имеет массив в качестве корневого значения. Чтобы правильно десериализовать JSON этой формы, мы должны дать команду  ObjectMapper десериализации предоставленного фрагмента JSON в  Collection (или подкласс),  а затем привести результат к  Collection объекту с соответствующим универсальным аргументом. Во многих случаях используется  List объект, как в следующем коде:

String json = "[{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":29,"
    + "\"address\":{\"street\":\"100 Elm Way\",\"city\":\"Foo City\","
    + "\"state\":\"NJ\",\"zipCode\":\"01234\"},\"isMale\":true},"
    + "{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"age\":27,"
    + "\"address\":{\"street\":\"200 Boxer Road\",\"city\":\"Bar City\","
    + "\"state\":\"NJ\",\"zipCode\":\"09876\"},\"isMale\":false}]";

ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("unchecked")
List<Person> people = (List<Person>) mapper.readValue(json, List.class);

System.out.println(people);

Выполнение этого фрагмента приводит к следующему выводу:

[{firstName=John, lastName=Doe, age=29, address={street=100 Elm Way, city=Foo City, state=NJ, zipCode=01234}, isMale=true}, {firstName=Jane, lastName=Doe, age=27, address={street=200 Boxer Road, city=Bar City, state=NJ, zipCode=09876}, isMale=false}]

Важно отметить , что бросок Использованный выше непроверенные литой , так как  ObjectMapper не может проверить , что общий аргумент  List является правильным априорно ; то есть, что  ObjectMapper фактически возвращая  List из  Person объектов , а не  List некоторых других объектов (или не  List совсем).

Чтение JSON из входного файла

Хотя полезно десериализовать JSON из строки, обычно желаемый JSON будет исходить из файла, а не из предоставленной строки. В этом случае  ObjectMapper может быть предоставлен любой   объект InputStream или  Fileобъект для чтения. Например, мы можем создать файл с именем  john.json в classpath, который содержит следующее:

{"firstName":"John","lastName":"Doe","alias":"Jay","age":29,"address":{"street":"100 Elm Way","city":"Foo City","state":"NJ","zipCode":"01234"},"isMale":true}

Затем для чтения из этого файла можно использовать следующий фрагмент:

ObjectMapper mapper = new ObjectMapper();
Person johnDoe = mapper.readValue(new File("john.json"), Person.class);
System.out.println(johnDoe);

Выполнение этого фрагмента приводит к следующему выводу:

Person [firstName=John, lastName=Doe, alias=null, age=29, isMale=true, address=Address [street=100 Elm Way, city=Foo City, state=NJ, zipCode=01234]]

Заключение

Хотя программирование является очевидной частью разработки программного обеспечения, оно не является единственным навыком, которым должны обладать разработчики. Во многих приложениях данные управляют приложением: файлы конфигурации создают динамизм, а данные веб-службы позволяют разным частям системы взаимодействовать друг с другом. В частности, JSON стал одним из де-факто  языков данных во многих приложениях Java. Изучение не только того, как структурирован этот язык, но и как включить JSON в приложения Java, может добавить важный набор инструментов в инструментарий разработчика Java.