Format JSON jest obecnie najpopularniejszym formatem do komunikacji pomiędzy aplikacjami, niezależnie od języka, w jakim są napisane. Jak pracować z formatem JSON w języku Java?
JSON to skrót, który rozwija się do JavaScript Object Notation. Przykładowe dane w tym formacie wyglądają następująco:
{ "name": "Paweł", "age": 37, "kids": [ "Alicja", "Gabriel", "Piotr" ], "company": { "name": "ClockworkJava", "url": "https://clockworkjava.pl" }, "married": true }
Dane muszą być zawarte w nawiasach klamrowych, potem mamy wpisy w postaci „klucz”: wartość, oddzielone przecinkami. Każdy klucz przekłada się na stan obiektu. Wartości może być „tekstem”, liczbą, wartością logiczną, tablicą ( [] ) bądź całym obiektem ( {} ).
Struktura powyższego przykładu przekłada się na poniższe klasy javowe.
package pl.clockworkjava.json; import java.util.List; public class Person { private String name; private int age; private List<String> kids; private Company company; private boolean married; public Person(String name, int age, List<String> kids, Company company, boolean married) { this.name = name; this.age = age; this.kids = kids; this.company = company; this.married = married; } }
package pl.clockworkjava.json; public class Company { private String name; private String url; public Company(String name, String url) { this.name = name; this.url = url; } }
Tak więc mamy dane w formacie JSON, mamy stosowne klasy w Javie. Jak z jednego świata przenieść się do drugiego i vice versa?
Mamy do tego dwa dedykowane, formalne API. Mianowicie jest to niskopoziomowe JSON-P oraz nowsze i wygodniejsze JSON-B.
Yasson
JSON-P i JSON-B to tylko API, czyli standard określający jaka metoda ma co robić i co zwracać. Potrzebna jest jeszcze konkretna implementacja. Jedną z lepszych jest Yasson, który implementuje zarówno JSON-P jak i JSON-B. Należy dodać go do zależności naszego projektu w pom.xml.
<dependencies> <!-- https://mvnrepository.com/artifact/org.eclipse/yasson --> <dependency> <groupId>org.eclipse</groupId> <artifactId>yasson</artifactId> <version>1.0.8</version> </dependency> </dependencies>
JSON-P
Poświęćmy chwilę na JSON-P, pomimo tego, że jest niskopoziomowy i obecnie mniej popularny to warto poznać podstawy, bo na nim bazuję nowszy JSON-B. Zwłaszcza że JSON-P ma jedna, ważną zaletę.
Zacznijmy od stworzenia JSON’a na podstawie dwóch obiektów
package pl.clockworkjava.json; import java.util.Arrays; public class App { public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); } }
JSON-P (podobnie zresztą jak JSON-B) pracuję na kilku klasach. Podstawową z nich jest JsonObject, reprezentuje ona, nomen omen, obiekt w formacie JSON. Dodajemy tam pary klucz – wartość manualnie.
package pl.clockworkjava.json; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; public class Company { private String name; private String url; public Company(String name, String url) { this.name = name; this.url = url; } public JsonObject toJSON() { JsonObject json = Json.createObjectBuilder() .add("name", this.name) .add("url", this.url).build(); return json; } }
Następnie używamy metody toJson, by utworzyć obiekt klasy JsonObject, a na nim możemy już wywołać toString.
package pl.clockworkjava.json; import java.util.Arrays; public class App { public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); System.out.println(cmp.toJSON().toString()); } }
{"name":"ClockworkJava","url":"https://clockworkjava.pl"}
Obiekt Person jest nieco bardziej skomplikowany, natomiast zasada jest tak sama – używamy buildera do stworzenia głównego obiektu, a następnie ręcznie dodajemy pary klucz wartość. Tu nowością jest użycie klasy JsonArrayBuilder do stworzenia elementu tablicy oraz dodanie całego JsonObiektu do klucza company.
package pl.clockworkjava.json; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import java.util.List; public class Person { private String name; private int age; private List<String> kids; private Company company; private boolean married; public Person(String name, int age, List<String> kids, Company company, boolean married) { this.name = name; this.age = age; this.kids = kids; this.company = company; this.married = married; } public JsonObject toJson() { JsonArrayBuilder builder = Json.createArrayBuilder(); this.kids.forEach(kid -> builder.add(kid)); JsonObject json = Json.createObjectBuilder() .add("name", name) .add("age", age) .add("kids", builder.build()) .add("company", this.company.toJSON()) .add("married", this.married).build(); return json; } }
package pl.clockworkjava.json; import java.util.Arrays; public class App { public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); System.out.println(person.toJson().toString()); } }
Czego wynikiem jest następujący, prawilny JSON:
{ "name":"Paweł", "age":34, "kids":["Alicja","Gabriel","Piotr"], "company" : { "name":"ClockworkJava", "url":"https://clockworkjava.pl" }, "married":true }
Do przeniesienia danych w formacie JSON na obiekt w Javie za pomocą JSON-P możemy użyć dwóch dróg. Wczytać cały tekst JSONa do pamięci i pobierać kolejne klucze. Druga opcja to przetwarzanie strumieniowe. Jest ono nieco bardziej skomplikowane, bo czyta ona danego JSON’a kawałek po kawałku, dzięki czemu nie obciąża tak bardzo pamięci oraz pozwala na pracę z baaaaaardzo dużymi (kilkaset megabajtów+) plikami JSON. Nie będę się na nim skupiał natomiast należy pamiętać iż mamy taką opcję, gdyby trafił nam się ów duży JSON (choć szczerze mówiąc mi się jeszcze nie trafił, w przeciwieństwie do sytuacji, gdy zdarza się pracować z xml).
Standardowy sposób wczytywania wygląda następująco:
public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); String stringified = person.toJson().toString(); // System.out.println(stringified); JsonReader jsonReader = Json.createReader(new StringReader(stringified)); JsonObject personJson = jsonReader.readObject(); String name = personJson.getString("name"); int age = personJson.getInt("age"); JsonArray kidsJson = personJson.getJsonArray("kids"); List<String> kids = kidsJson .stream() // -> JsonValue("Alicja") .map(JsonValue::toString)// -> "Alicja" .map(kidName -> kidName.replaceAll("\"", "")) // -> Alicja .collect(Collectors.toList()); String companyName = personJson.getJsonObject("company").getString("name"); String companyUrl = personJson.getJsonObject("company").getString("url"); boolean married = personJson.getBoolean("married"); jsonReader.close(); Company newCompany = new Company(companyName, companyUrl); Person newPerson = new Person(name, age, kids, newCompany, married);
Sprowadza się więc wszystko do wskazywania palcem, jaki element struktury JSON nas interesuje, przypisania go do zmiennej, a potem stworzenia na tej podstawie obiektu.
JSON-B
Nowszym rozwiązaniem jest JSON-B – Java API for JSON Binding. Słowo binding jest tu kluczowe, bo JSON-B pozwala, w prostych przypadkach, na niemal automatyczne przeniesienie obiektu na format JSON i vice versa. Niestety wymaga to publicznego dostępu to pól, które chcemy, by w JSONie się znalazły, czyli albo robimy publiczny stan, albo zestaw getter/setter, co na jedno pod względem enkapsulacji wychodzi. Dodałem więc gettery i settery do klas Person i Company.
public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); Jsonb jsonb = JsonbBuilder.create(); String stringified = jsonb.toJson(person); System.out.println(stringified); }
To tyle. Wystarczy utworzyć obiekt Jsonb oraz wywołać metodę .toJson. W przypadku tworzenia obiektu na podstawie danych w formacie JSON potrzebujemy, oprócz dostępu do pól, jeszcze domyślny (bezargumentowy) konstruktor. Dodałem więc takie do klas Company i Person. Wówczas wystarczy wywołać już tylko metodę fromJson.
public static void main(String[] args) { Company cmp = new Company("ClockworkJava", "https://clockworkjava.pl"); Person person = new Person("Paweł", 34, Arrays.asList("Alicja", "Gabriel", "Piotr"), cmp, true); Jsonb jsonb = JsonbBuilder.create(); String stringified = jsonb.toJson(person); System.out.println(stringified); Person newOne = jsonb.fromJson(stringified, Person.class); System.out.println(newOne); }
Wynik działania powyższego kodu jest następujący:
{"age":34,"company":{"name":"ClockworkJava","url":"https://clockworkjava.pl"},"kids":["Alicja","Gabriel","Piotr"],"married":true,"name":"Paweł"} Person{name='Paweł', age=34, kids='Alicja,Gabriel,Piotr', company=Company{name='ClockworkJava', url='https://clockworkjava.pl'}, married=true}
Efekt ten sam, a zdecydowanie mniejsza ilość kodu była potrzebna. Kosztem dodania publicznego dostępu do pól oraz utworzenia domyślnych konstruktorów.
Mapowanie nazw pól
W prawdziwym świecie nie zawsze nazwy pól obiektu przekładają się 1:1 na pola w JSONie. Na przykład dla firmy mamy JSONa z polem „addr” zamiast „url”.
{"addr":"https://clockworkjava.pl","name":"ClockworkJava"}
Przy standardowych ustawieniach powyższy JSON zostanie przekształcony na następujący obiekt, z polem url o wartości null.
Company{name='ClockworkJava', url='null'}
Do zaradzenia tej sytuacji używamy adnotacji @JsonProperty, by samemu podać nazwę odpowiedniego pola. Poniższa definicja klasy Company będzie już działać dla formatu JSON z polem „addr” zamiast „url”:
package pl.clockworkjava.json; import javax.json.Json; import javax.json.JsonObject; import javax.json.bind.annotation.JsonbProperty; public class Company { private String name; @JsonbProperty("addr") private String url; public Company(String name, String url) { this.name = name; this.url = url; } public Company() { } public JsonObject toJSON() { JsonObject json = Json.createObjectBuilder() .add("name", this.name) .add("url", this.url).build(); return json; } @Override public String toString() { return "Company{" + "name='" + name + '\'' + ", url='" + url + '\'' + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } }
Adnotacje umieścić możemy również na poziomie gettera bądź settera. Wówczas inna nazwa pola będzie używana tylko przy zapisie do JSONa przy getterze, a odczycie z JSONa przy setterze.
By być na bieżąco i mieć realny wpływ na tematykę tworzonych przeze mnie artykułów zapraszam do dołączenia do mojego newslettera.
hej, dlaczego uzywasz tutaj Yasson a nie standardowo jak wszyscy ObjectMappera z biblioteki Jackson?
Yasson jest referencyjną implementacja JSON-P JSON-B, tylko dlatego 🙂 Plus nie szedłbym w tak ogólne „jak wszyscy” 😉
Literówka:
„użycie klasy JsonArrayBuilder to stworzenia elementu”
Dzięki!