JSON Binding PRD 1.0, JSR 367 İncelemesi

jsonb-logoo
JSONB: Java API for JSON Binding

Bir Java EE 8 standardı olan JSON-B kütüphanesinin public review draft (PR) ile gelen özelliklerine birlikte göz atacağız. JSR 367 – JSON Binding, yapılan anket sonucu Java EE 8 platformu altında en çok istenen standart olmuştu. JSON-P standardıyla birlikte JSON-B, Java platformunda JavaScript Object Notation (JSON) odaklı bir diğer önemli kütüphane olmakta. Aslında aynı işlemler XML yapısı için de daha önceki yıllarda 2 farklı standartla birlikte hayata geçirilmişti: JAXP (Java API for XML Processing) ve JAXB (Java API for XML Binding) kütüphaneleri.

JSONB (ya da JSON-B, JSON Binding), JSON dokümanları ve Java nesneleri arasında çift yönlü (serialization ve deserialization) dönüştürümü yerine getirmektedir. JSONB, varsayılan konfigürasyonu değiştirme, JSON Processing (JSON-P) 1.1 kütüphanesiyle entegrasyonu sağlama ve parçalı olarak JSON dokümanınlarını Java nesnelerine eşleme gibi önemli ve etkili özellikleriyle birlikte gelmektedir. JSON-B, Jackson, GSON, Genson ve JSON-IO gibi 3.parti alternatif kütüphanelerin yerini alacak bir kütüphane olmaktadır. Projenin asıl amacı bahsi geçen 3.parti projeleri incelemek, analiz etmek ve içlerinden en önemlilerini çıkararak bir standart oluşturmaktır. JSON işlemleri bünyesinde artık 3. parti kütüphanelere ve farklı konfigürasyonlara gerek duyulmayacak bir yapıdan söz etmekteyiz. JSONB, Dmitry Kornilov liderliğiyle ilerlemesini sürdürmeye devam ediyor. Aşağıdaki resimde bir JSR standardının başlangıç ve bitiş akışını görmekteyiz. Şu an spesifikasyon kırmızıyla çevrelenmiş evrede bulunmakta ve bu evreyle birlikte açık olan konuların geliştirilmesi ve sonuçlanması sürdürülmektedir.

Kütüphanenin şu an bulunduğu evre

Referans implementasyona ulaşmak ve projenizde denemek için ilgili sayfadaki yönergeleri uygulamanız yeterli olacaktır. Ayrıca örnek bir JSONB kullanımını da inceleyebilirsiniz.

JSONB Engine

JSONB API kullanımının başlangıç noktası JsonbBuilder arayüzüdür. Bu arayüz ile varsayılan konfigürasyon ve provider değerleri dışında farklı düzenlemelerle Jsonb nesnesi oluşturabiliriz. Jsonb arayüzünü implemente eden JsonbBinding sınıfı örneklenerek API kullanımının ilk adımı atılmış oluyor. JsonbBinding sınıfını da JsonBindingBuilder sınıfını kullanarak örnekliyoruz. Ayrıca JsonbBuilder arayüzündeki statik metodları kullanarak da istediğimiz esas sınıfı (Jsonb) kullanabiliyoruz. Jsonb sınıfı projenin genelinde sadece bir kere tanımlanmasına dikkat edilmelidir.

Serialization ve deserialization JSON Binding (Bağlam/bağlama) operasyonları üzerinde soyutlama sağlayan Jsonb arayüzü aşağıdaki overloaded metodları sunmaktadır:

javax.json.bind.Jsonb
// deserialize
<T> T fromJson(java.lang.String, java.lang.Class<T>)
<T> T fromJson(java.lang.String, java.lang.reflect.Type)
<T> T fromJson(java.io.Reader, java.lang.Class<T>)
<T> T fromJson(java.io.Reader, java.lang.reflect.Type)
<T> T fromJson(java.io.InputStream, java.lang.Class<T>)
<T> T fromJson(java.io.InputStream, java.lang.reflect.Type)

// serialize
String toJson(java.lang.Object)
String toJson(java.lang.Object, java.lang.reflect.Type)
void toJson(java.lang.Object, java.io.Writer)
void toJson(java.lang.Object, java.lang.reflect.Type, java.io.Writer)
void toJson(java.lang.Object, java.io.OutputStream)
void toJson(java.lang.Object, java.lang.reflect.Type, java.io.OutputStream)

Serialization JSON: (Yazma işlemi) Nesne içeriğinin JSON yapısına yansıtılmasıdır.

Deserialization JSON: (Okuma işlemi) Serialization işleminin tersi. JSON değerlerinin nesne yapısına birebir aktarımıdır.

binding toJson ve fromJson kullanımı
public class Dog {
    public int age;
    public String name;
    public String breed;
    public int lifeSpan;

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", breed='" + breed + '\'' +
                ", lifeSpan=" + lifeSpan +
                '}';
    }
}

public void JsonbBindingTest(){
    Dog mars = new Dog();
    mars.name = "mars";
    mars.age = 3;
    mars.breed = "Greyhound";
    mars.lifeSpan = 12;

    final Jsonb jsonb = (new JsonBindingBuilder()).build(); (1)
    String result = jsonb.toJson(mars); (2)
//  {                                   (3)
//      "age":3,
//      "breed":"Greyhound",
//      "lifeSpan":12,
//      "name":"mars"
//  }

    Dog dog = jsonb.fromJson(
    "{"age":3,"breed":"Greyhound","lifeSpan":12,"name":"mars"}", Dog.class (4)
    );
//  Dog{name='mars', age=3, breed='Greyhound', lifeSpan=12} (5)
}
1 varsayılan ayarlarda bir Jsonb nesnesi oluşturuyoruz. İstersek JsonbBuilder.create() üzerinden de hedefe ulaşabilirdik.
2 Örneklenen Dog sınıfına serialize işlemi uyguluyoruz. public erişime sahip değişkenlerin getter üniteleri olmadan serialize işlemi gerçekleşiyor.
3 JSON doküman çıktısını görmekteyiz. Dikkatli incelersek, alan dizilimi Dog sınıfındaki değişken sıralamasıyla aynı olmadığını görmekteyiz. Sıralama Lexicographical sözlük sıralamasına göre yapılmaktır. Ama JSON içindeki alan sıralamalarına müdahale edebiliyoruz. Bununla ilgili örnekleri de sizlerle paylaşacağım.
4 Bu sefer tam tersini uyguluyoruz. JSON dokumanı içeriğini hedefte belirtilen Dog sınıfına bağlıyoruz. Gerekli alan değerleri bu sınıfa aktarılacak.
5 toString üzerinden desealize işleminin gerçekleştiğini görmekteyiz.

Bilindik bazı eşlemeler hakkında da aşağıdaki tabloyu sizlerle paylaşmak istiyorum. Bu veri tipleri gibi tarih ve collection tiplerinin de eşlenmesi söz konusu. JSON tarafında toplam 6 farklı veri tipi bulunmaktadır: string, null, boolean, sayı (tamsayı ve ondalık sayı), nesne (JSON doküman) ve dizi. Bu tiplere atanacak uygun eşlemeler tespit edilerek serialization ve deserialization yöntemleri gerçekleşir.

Tablo 1.Varsayılan Eşlemeler
Tipler Eşleme

java.lang.String

"String"

"String"

java.lang.Character

‘\u0041’

"A"

java.lang.Byte (byte)

(byte)1

1

java.lang.Short (short)

(short)1

1

java.lang.Integer (int)

(int)1

1

java.lang.Long (long)

1L

1

java.lang.Float (float)

1.2f

1.2

java.lang.Double (double)

1.2

1.2

java.lang.Boolean (boolean)

true

true

Serialization yönteminde yukarıdaki tiplerin dönüşümü toString() metoduyla yapılmaktadır. Deserialization yönteminde ise, ilgili tiplere ait parse metodları tektikleniyor.
Java tiplerinin ve JSON tiplerinin karşılıklı dönüştürümüne yönelik örnekler:
final Jsonb jsonb = JsonbBuilder.create(); (1)

final Collection<Integer> collection = Arrays.asList(1, 2, 3);
> jsonb.toJson(collection); // [1,2,3] (2)

final Collection<Number> collectionNumbers = Arrays.asList(1, 2.2, 3.0f, new BigDecimal("1.2"));
> jsonb.toJson(collectionNumbers); // [1,2.2,3.0,1.2] (3)

LinkedList<String> names = new LinkedList<>(); (4)
names.add("Rahman");
names.add("Göksel");
> jsonb.toJson(linkedList); // ["Rahman","Göksel"] (5)
LinkedList<String> result = jsonb.fromJson("[\"Rahman\",\"Göksel\"]", new LinkedList<String>() {}.getClass().getGenericSuperclass()); (6)
Iterator<String> iterator = result.iterator(); // Rahman, Göksel

final String[] stringArray = {"elma", "armut", "kel_mahmut"};
> jsonb.toJson(stringArray); // ["elma","armut","kel_mahmut"] (7)

Map<String, Integer> exams = new LinkedHashMap<>(); (8)
exams.put("Supermen",10);
exams.put("Batman",20);
exams.put("Wolverine",30);
> jsonb.toJson(exams); // {"Supermen":10,"Batman":20,"Wolverine":30}

List somethingList = new ArrayList(); (9)
somethingList.add("birinci");

Dog greyhound = new Dog();
greyhound.name = "mars";
greyhound.age = 3;
greyhound.breed = "Greyhound";
greyhound.lifeSpan = 12;
somethingList.add(greyhound);

> jsonb.toJson(somethingList); // [ "birinci", {"age":3, "breed":"Greyhound", "lifeSpan":12, "name":"mars"} ]

final String[][] stringMultiArray = {{"elastic", "mongo"},{"hadoop", "javascript"}}; (10)
> jsonb.toJson(stringMultiArray)); // [["elastic","mongo"],["hadoop","javascript"]]

String json = "[\"dog\",\"cat\",\"hamster\"]";
> List result = jsonb.fromJson(json, List.class); (11)
result.get(0); // dog
1 Varsayılan değerleriyle bir Jsonb nesnesi oluşturuyoruz.
2 Collection tipinde bir dizimiz mevcut. Bunun JSON dönüşümünü yapıyoruz.
3 Eğer sayı bazında genelleme yaparak bir koleksiyon hazırlarsak, JSON dönüşümleri görüldüğü gibi ya ondalık ya da tamsayı olarak yansıtılmaktadır.
4 LinkedList, LinkedHashSet ve Deque gibi collection tipindekileri JSON formatına dönüştürebilmekteyiz.
5 2 farklı isim JSON formatında dizi içerisinde yazılmaktadır.
6 Aynı şekilde, fromJSON metodunu kullanırsak bir JSON dizisini direkt bir collection türünde elde edebiliyoruz.
7 One dimensional (tek boyutlu) dizileri JSON dizisi olarak yakalayabiliyoruz.
8 Map tanımlamaları JSON tipinde dokümana {} işaret etmektedir.
9 List içerisinde farklı değerler ekleyerekde JSON formatında o değerlere karşılık gelen tipteki dönüştürümler döndürülür.
10 2 boyutlu bir dizinin JSON dönüşümüne dair bir örnek.
11 fromJson metodu bir JSON dizisini List arayüzünü implemente eden ArrayList nesnesine dönüştürür. Böylelikle, direkt List arayüzüne eşitlenmesi bir sorun çıkartmamaktadır.

Değişken erişimleri

Serialize işleminde JSONB API bazı durumlarda nesne değişkenlerini es geçmektedir. null, transient ve static karakterlere sahip değişkenler JSON eşleşmesinde dikkate alınmazlar.

transient, static ve null değer gösterimi
public class Dog {

    public String name;
    public static int age;
    public final String breed;

    @JsonbTransient         (1)
    public int lifeSpan;

    public Dog(String breed) {
        this.breed = breed;
    }
}

Dog greyhound = new Dog("Greyhound");
greyhound.age = 3;
greyhound.lifeSpan = 12;

jsonb.toJson(greyhound); // {"breed":"Greyhound"} (2)
1 ilgili alan:değer çiftinin JSON dokümanında gözükmesini istemediğimizi söylüyoruz.
2 Sonuç olarak sadece breed alanı bize dönüyor. final karaktere sahip alanlar serialize olabiliyor. Onun dışındaki değerlerde name değişkenine değer ataması olmadığından bu alan null olarak gözükmektedir. static değere sahip age alanı dönüştürümde dikkate alınmıyor. lifeSpan alanı transient olduğu için de pas geçiliyor.
null ve static değerlerin yazma işleminde gözükmesi
@JsonbNillable          (1)
public class Dog {
    ...
    ...
}

Dog greyhound = new Dog("Greyhound");
greyhound.age = 3;
greyhound.lifeSpan = 12;

jsonb.toJson(greyhound); // {"age":null,"breed":"Greyhound","name":null (2)
1 Varsayılan notasyon değeri true olmaktadır. Hem paket üzerinde hem de sınıflarda ve arayüzlerde bu notasyon kullanılabilir. Örneğimizde; JsonbNillable notasyonuyla birlikte JSON içine serialize olan sınıfın null değere sahip alanlarının gözükmesi istenmektedir. Diğer bir tanımlama biçimi şu şekilde yazılabilir: @JsonbNillable(value = true) veya @JsonbNillable(true).
2 age ve name alanlarının null değerleriyle JSON dokümanında bulunduğunu görmekteyiz.
public bir alanın JSON karşılığı
public class Deadline {
    @JsonbDateFormat("dd-MM-yyyy--ss:mm:HH") (1)
    public LocalDateTime date; (2)
}

Deadline deadline = new Deadline();
deadline.date = LocalDateTime.now();

jsonb.toJson(deadline); // {"date":"17-08-2016--59:56:12"} (2)
1 @JsonbDateFormat notasyonu varsayılan tarih formatı dışında kendi özel tarih formatımızı uygulamamız için kullanılmaktadır. Alan, metod, tip, paket ve parametre kısımlarında kullanılabilir.
2 getter metodsuz public erişime sahip değişkene sahip bir sınıfı kullanıyoruz.
3 alan ismi date olarak tarih bilgisine ulaşıyoruz.
sadece getter metoduna göre döndürülen değer ve alan ismi
public class Deadline {
    private LocalDateTime date;
    public LocalDateTime getExamDate() { (1)
        return date;
    }
    public void setDate(LocalDateTime date) {
        this.date = date;
    }
}

Deadline deadline = new Deadline();
deadline.setDate(LocalDateTime.now());

jsonb.toJson(deadline); // {"examDate":"2016-08-16T22:26:44.023"} (2)
1 serialize aşamasında getter metoduna bakılacak. Getter metodunun get* isminden sonraki kısım JSON alanının ismi olacak. Sınıfın date değişkeninin dışarıya kapalı olduğunu da unutmayalım. Eğer public erişime sahip olsaydı date değişkeni, JSON dokümanında hem date hem de examDate alanlarını görürdük. Serialize noktasında setDate metoduna bakılmamaktadır.
2 tarih değerini tutan JSON alanının ismi examDate olarak yansımaktadır.
Optional<T> eşleşmesi
final Jsonb jsonb = (new JsonBindingBuilder()).build();

final OptionalInt[] array = {OptionalInt.of(1), OptionalInt.of(2), OptionalInt.empty()};
jsonb.toJson(array); // [1,2,null] (1)

final Optional[] arrayOpt = {Optional.ofNullable(null), Optional.of(2), Optional.empty()};
jsonb.toJson(array1); // [null,2,null] (2)
1 Optional tiplerde boş ve null değer atanmadıkça değerlerin JSON karşılığı yazmada gösterilmektedir. Örneğimizde OptionalInt.empty() JSON karşılığında null olarak atanmaktadır.
2 ofNullable ve empty metodlarının kullanımında ilgili dizi elemanlarının null döndüğünü görüyoruz.

JSONB Config

JSONB kütüphanesinin varsayılan konfigürasyon değerlerine müdahale edebiliyoruz. Konfigürasyon ayarları dışında yardımcı notasyonlarla da yazma operasyonlarında değişiklikler yapabiliyoruz. Başlıca hangi adımlar mevcut görelim:

pretty print format 1
final JsonbConfig config = new JsonbConfig().withFormatting(true); (1)
final Jsonb jsonb = JsonbBuilder.create(config);
>jsonb.toJson(Arrays.asList("jsonb", "jsonp"));
//[
//    "jsonb",
//    "jsonp"
//]
1 JSON içeriği varsayılan yazma işlemi sonucunda aralık bırakılmadan dönmektedir. JsonbConfig sınıfıyla kullanılan withFormatting metodu ile birlikte JSON içeriğini daha anlaşılabilir formatta çıktılatabiliyoruz.
pretty print format 2
final JsonbConfig config = new JsonbConfig();
config.setProperty(JsonbConfig.FORMATTING, Boolean.TRUE); (1)
final Jsonb jsonb = JsonbBuilder.create(config);
>jsonb.toJson(Arrays.asList("jsonb", "jsonp"));
//[
//    "jsonb",
//    "jsonp"
//]
1 withFormatting metodu dışında setProperty metoduyla ilgili anahtar:değer çiftini belirterek de aynı hedefe ulaşırız.
Lexicographical Order
public class Dog {

    public String name;
    public String breed;
    public int age;
    public int lifeSpan;
}

final JsonbConfig jsonbConfig = new JsonbConfig()
                        .withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL);(1)
Jsonb jsonb = JsonbBuilder.create(jsonbConfig);
Dog greyhound = new Dog();
greyhound.name = "mars";
greyhound.age = 3;
greyhound.breed = "Greyhound";
greyhound.lifeSpan = 12;

jsonb.toJson(greyhound);
//{"age":3,"breed":"Greyhound","lifeSpan":12,"name":"mars"}
1 Varsayılan sözlük sırasında JSON değişkenleri sıralanır.
Reverse Order
final JsonbConfig jsonbConfig = new JsonbConfig()
                        .withPropertyOrderStrategy(PropertyOrderStrategy.REVERSE);(1)
...
jsonb.toJson(greyhound);
//{"name":"mars","lifeSpan":12,"breed":"Greyhound","age":3}
1 Lexicographical sıralamanın tersi olmaktadır.
Any Order
final JsonbConfig jsonbConfig = new JsonbConfig()
                        .withPropertyOrderStrategy(PropertyOrderStrategy.ANY);(1)
...
jsonb.toJson(greyhound);
//{"lifeSpan":12,"name":"mars","breed":"Greyhound","age":3}
1 JSON alanları herhangi bir sıralama stratejisine bağımlı kalmadan sıralanır.
JSON alanlarına özel sıralama özelliği
@JsonbPropertyOrder({"name", "age", "breed" ,"lifeSpan"}) (1)
public class Dog {

    public String name;
    public String breed;
    public int age;
    public int lifeSpan;
}
...
jsonb.toJson(greyhound);
// {"name":"mars","age":3,"breed":"Greyhound","lifeSpan":12}
1 JsonbPropertyOrder notasyonuyla birlikte JSON alan sıralamalasını kendimiz düzenleyebiliyoruz. Eğer bu notasyonu kullanırsak, yukarıdaki 3 sıralama stratejisi ilgili tiplerde dikkate alınmamaktadır. Bu özellik için JsonbConfig nesnesine ihtiyaç olmamaktadır. Bu notasyonla istenmeyen değişkenlerin serialize olmasını da engelleyebiliyoruz. Örneğin, sadece name ve age alanlarının JSON dokümanına yazılmasını istersek: @JsonbPropertyOrder({"name", "age"}) yapılması yeterlidir.
JsonbProperty kullanımı
public class Dog {

    @JsonbProperty(value = "dogName") (1)
    public String name;
    @JsonbProperty(nillable = false) (2)
    public String breed;

    public int age;
    public int lifeSpan;
}

Jsonb jsonb = JsonbBuilder.create();
Dog greyhound = new Dog();
greyhound.name = "mars";
greyhound.age = 3;
greyhound.lifeSpan = 12;

jsonb.toJson(greyhound);
// {"age":3,"dogName":"mars","lifeSpan":12} (2)
1 JsonbProperty notasyonu 2 özelliğe sahiptir. value alanını kullanarak serialize noktasındaki JSON alanının isminin dogName olmasını istediğimizi söylüyoruz.
2 Diğer bir özellik ise nillable alanıyla birlikte gelmekte. Bu alanı kullanarak null değere sahip olursa değişkenin, JSON içeriğinde gösterilmemesini istiyoruz.
3 Çıktıyı incelediğimizde görüyoruz ki, name değişkeninin ismi dogName olarak gözükmektedir ve breed alanı null değere sahip olduğu için içerikte o alan ve değeri dikkate alınmamıştır.

Son olarak kendi adaptörümüzü/çeviricimizi nasıl tasarlayıp projemizde kullanabiliriz görelim. Örneğimiz tarih tipindeki bir değişkeni MongoDB veritabanının tanıdığı BSON veri tiplerinden date yapısının söz dizimine uyarlamakla ilgili olacak.

Çevirici kullanımı
public class Operator {
    @JsonbProperty(value = "$date")
    public String date;
}

JsonbAdapter<?, ?>[] adapters = {
        new JsonbAdapter<LocalDateTime, Operator>() { (1)
            @Override
            public Operator adaptToJson(LocalDateTime date) { (2)
                Operator operator = new Operator();
                operator.date = date.toString();
                return operator;
            }

            @Override
            public LocalDateTime adaptFromJson(Operator op) {  (3)
                return LocalDateTime.parse(op.date);
            }
        }
};
Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
        .withAdapters(adapters)
        .withFormatting(true));
jsonb.toJson(LocalDateTime.now());
1 Öncelikle yeni bir JsonbAdapter oluşturuyoruz.
2 adaptToJson metodu sadece serialization (yazma) işleminde kullanılmaktadır. Metod içerisinde dönüştürüme dair iş mantığı bulunmalı. Orijinal olan tipten (LocalDateTime) adapte olunacak tipe (Operator) bir dönüştürüm gerçekleşecek. Dönüştürüm sonrası uyarlanmış tip JSON tarafına eşitlenecek.
3 adaptFromJson metodu sadece deserialization (okuma) işleminde kullanılmaktadır. Uyarlanacak tipten (Operator) orijinal tipe (LocalDateTime) bir dönüştürüm gerçekleşecek. Dönüştürüm sonrası orijinal tip nesne tarafına eşitlenecek.
Yazma işleminden sonraki sonuç:
{
    "$date":"2016-08-17T22:05:44.430"
}

Yukarıdaki JSR evresinde gördüğümüz gibi JSON-B tamamlanma evresine gelene kadar geliştirime açık bir kütüphane olmakta. Geliştirme sürecinde mevcut tüm açık konulara bu bağlantıdan ulaşabilirsiniz:

İlgili referans gösterilen implementasyonu buradan elde edilebilir:

JSON-B geliştirilmesi süren bir spesifikasyon, yayınlanan implementasyonda İstanbul Java User Grubunun bir parçası olup teknik geri bildirimlerinizi ulaştırabilirsiniz. Tek yapılması gerek adım aşağıdaki adres üzerinden I’m [isim], from Istanbul JUG kalıbını ekleyip, konu başlığına [Adopt-a-JSR] etiketini de belirterek düşüncelerinizi ve/veya gördüğünüz problemleri paylaşabilirsiniz:

Hakan
İstanbul JUG

Bir Cevap Yazın