Практикум по составлению json-схем

В статье “Генерируем бины по json описанию” Кирилл познакомил нас с мавен-плагином org.jsonschema2pojo:jsonschema2pojo-maven-plugin.

В заметке “От json к json-схемам” рассказано о том, как генерировать бины из json, json-схем и о том, в каких ситуациях json-схемы предпочтительней для этой генерации.

Далее хочу поделиться с вами свои опытом по составлению сложных json-схем.

1. Об опции плагина <useLongIntegers>

Тип объекта type в json-схеме может принимать следующие значения array, boolean,integer,number,null,object,string. (JSON Schema: core definitions and terminology)

Попробуем составить json-схему для следующей json выдачи с использованием только стандартных type:

{  
   "ts":1419503797,
   "ip":"194.186.25.31",
   "link":0,
   "ua":"Mozilla/5.0",
   "authid":{  
      "ip":"10.10.10.10",
      "host":"http://ho-st.host12324324.host"
   }
}

Такая json-схема будет иметь следующий вид:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{  
   "type":"object",
   "properties":{  
      "ts":{  
         "type":"integer"
      },
      "ip":{  
         "type":"string"
      },
      "link":{  
         "type":"integer"
      },
      "ua":{  
         "type":"string"
      },
      "authid":{  
         "type":"object",
         "properties":{  
            "ip":{  
               "type":"string"
            },
            "host":{  
               "type":"string"
            }
         }
      }
   }
}

Что нужно знать, о настройке плагина <useLongIntegers>?

Она влияет на переменные, чей "type"равен "integer".

Вот в чём дело: если useLongIntegers=false, то переменные link и ts, тип которых мы указали как integer, в сгенерированных бинах будут приведены к java.lang.Integer; если же выставим useLongIntegers=true, то переменные link и ts будут типа java.lang.Long.

Так что, когда вы работаете с большими числами в json-е, держите useLongIntegers включенным.

2.Подменяем стандартные type на javaType

Как видно из предыдущего примера, настройка useLongIntegers позволяет сгенерировать все переменные с "type":"integer" либо в java.lang.Long, либо в java.lang.Integer.

А что же делать, когда необходимо одну переменную получить типа Long, а вторую типа Integer? В такой ситуации удобно использовать javaType.

javaType позволяет нам явно указать, что мы хотим получить в бинах в качестве типа переменной. Json-схема с использованием javaType может выглядеть следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{  
   "type":"object",
   "properties":{  
      "ts":{  
         "type":"object",
         "javaType":"java.lang.Long"
      },
      "ip":{  
         "type":"string"
      },
      "link":{  
         "type":"object",
         "javaType":"java.lang.Integer"
      },
      "ua":{  
         "type":"string"
      },
      "authid":{  
         "type":"object",
         "properties":{  
            "ip":{  
               "type":"string"
            },
            "host":{  
               "type":"string"
            }
         }
      }
   }
}

3.Увидел большую json-схему? Испугайся и разбей на несколько!

Честно признаюсь, когда я впервые увидел json-схему, мне она показалась невероятно громоздкой. Дело в том, что json-схема всегда будет объемней того json-а, который она описывает. И для крупных json-ов соответствующие им схемы нужно разбивать.

Разбить json-схему на несколько файлов и сохранить при этом связность схемы можно с помощью "$ref". (JSON Schema: core definitions and terminology) Так, например, представленную выше json-схему можно разнести по двум файлам следующим образом:

1) main.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{  
   "type":"object",
   "properties":{  
      "ts":{  
         "type":"object",
         "javaType":"java.lang.Long"
      },
      "ip":{  
         "type":"string"
      },
      "link":{  
         "type":"object",
         "javaType":"java.lang.Integer"
      },
      "ua":{  
         "type":"string"
      },
      "authid":{  
         "$ref":"authid.json"
      }
   }
}

2)authid.json

1
2
3
4
5
6
7
8
9
10
11
12
{  
   "id":"#authid",
   "type":"object",
   "properties":{  
      "ip":{  
         "type":"string"
      },
      "host":{  
         "type":"string"
      }
   }
}

4. Встраиваем с помощью javaType сгенерированные классы

В продолжении темы о разделении json-схем на части,отмечу очень важный бонус javaType: в качестве javaType может быть указан объект, сгенерированный с помощью jsonschema2pojo по другой json-схеме и json-у. Так у нас появляется возможность структурировать наши объекты ещё более гибко.

Вернемся к уже знакомому нам по первой части pets.json:

{  
   "cat":{  
      "name":"Nancy",
      "food":"fish",
      "age":20,
      "kittens":[  
         "Marry",
         "Carry"
      ]
   },
   "dog":{  
      "name":"Sid",
      "food":"meat"
   }
}

Для него мы можем сгенерировать объекты по совершенно иному принципу!

Легко видеть, что части pets.json, где перечисляются name,food,age одинаковые по своему смыслу, это свойства животных кошки и собаки. Поэтому неплохо было бы сгенерировать класс PetsProperties, описывающий эти свойства. Для его генерации вынесем в отдельную json-схему pets-properties.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{  
         "type":"object",
         "properties":{  
            "name":{  
               "type":"string"
            },
            "food":{  
               "type":"string"
            },
            "age":{  
               "type":"integer"
            },
            "kittens":{  
               "type":"array",
               "minItems":1,
               "items":{  
                  "type":"string"
               },
               "uniqueItems":true
            }
         }
}

Выставляем в плагине <sourceType>jsonschema</sourceType>; в <targetPackage> укажем com.example; кладем наш файлик pets-properties.json в <sourceDirectory>; компилируем проект и получаем в папке <outputDirectory> сгенерированный набор класс com.example.PetsProperties с полями String name, String food, Integer age, List kittens.

Этот класс нам пригодится, когда мы будем окончательно описывать pets.json с помощью вот такой json-cхемы (назовем его new-pets.json):

1
2
3
4
5
6
7
8
9
10
11
{
  "type": "object",
  "properties": {
            "cat": {  "type": "object",
                      "javaType": "com.example.PetsProperty"
            },
            "dog": {  "type": "object",
                      "javaType": "com.example.PetsProperty"
            }
   }
 }

Аналогично описанному выше способу, получим объекты. Заметим, что сгенерированный класс com.example.NewPets будет содержать в себе два объекта cat и dog, только типа они будут одного com.example.PetsProperty (А не com.example.Cat и com.example.Dog, как мы получали раньше). Чем это удобней? Получаем меньше избыточности, меньше сгенерированного кода.

5. Обуздать коллекции вложенные друг в друга

Иногда приходится работать со сложными конструкциями в json,состоящими из объектов, в которых поля - это произвольные ключи, а их значение - произвольные объекты и т.д.

Такие сложные json нам требуется нередко представить в виде вложенных друг в друга коллекций: списков, состоящие из других списков, и map состоящие из других map и т.д

Стандартными методами json-schema можно описывать коллекции большой степени вложенности с помощью "type": "array":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(фрагмент)
{  
   "kittens":{  
      "type":"array",
      "uniqueItems":true,
      "minItems":1,
      "items":{  
         "type":"array",
         "minItems":1,
         "uniqueItems":true,
         "items":{  
            "type":"array",
            "minItems":1,
            "uniqueItems":true,
            "items":{  
               "type":"string"
            }
         }
      }
   }
}

Результатом такой json-схемы станет поле Set<Set<Set<String>>> kittens = new LinkedHashSet<Set<Set<String>>>() в сгенерированном классе. Если убрать из схемы опции "uniqueItems":true, то получим List<List<List<String>>> kittens= new ArrayList<List<List<String>>>().

Иногда требуется обыграть в json-е наличие структур типа Map.

Вот, например, есть фрагмент такого json:

{  
   "IPDATA":{  
      "date":"2014-12-25T12:54:03.433793",
      "iplist":{  
         "session:1411560095565656":[  
            "10.10.10.10",
            "10.10.10.9"
         ],
         "session:1411560095565659":[  
            "10.10.10.10"
         ]
      }
   }
}

Для построения соответствующей json-схемы могу посоветовать использовать уже знакомый нам javaType:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{  
   "id":"#ipdata",
   "type":"object",
   "properties":{  
      "IPDATA":{  
         "type":"object",
         "properties":{  
            "date":{  
               "type":"string"
            },
            "iplist":{  
               "type":"object",
               "javaType":"java.util.Map<String, java.util.List>"
            }
         }
      }
   }
}

Начиная с версии 0.4.8 jsonschema2pojo-плагина с помощью javaType можно задавать коллекции высокой степени вложенности, например: "javaType": "java.util.Map<String, java.util.Map<String, String>>", "javaType": "java.util.Map<String, java.util.Map<String, com.example.MyOwnBean>>".

6.Обуздать сложные конструкции с помощью кастомных десериализаторов

В предыдущем пункте я не просто так указал версию плагина, при котором javaType умеет работать с коллекциями высокой степени вложенности, в версиях 0.4.7 и ниже javaType не позволял сделать такого. Мне в таком случае потребовалось написать собственный десериализатор.

Покажу это на примере, есть такой json:

{  
   "IPDATA":{  
      "data":{  
         "session1:1411560095":{  
            "0":{  
               "cnt":2,
               "last":1419589877000
            },
            "1":{  
               "cnt":1,
               "last":1419589877001
            }
         },
         "session2:1411560091":{  
            "0":{  
               "cnt":1,
               "last":1419589877003
            }
         }
      }
   }
}

Что мы здесь видим?

Внутри data есть map, ключами которой являются элементы session1:1411560095,session2:1411560091. Внутри session1:1411560095 есть map, ключами которой являются элементы "0", "1". Для session2:1411560091 аналогично.

Очевидно, что для элементов вида {"cnt": 1,"last": 1419589877000} можно сразу составить отдельную json-схему session-detailed-information-bean.json и сгенерировать объект SessionDetailedInformationBean.

Схема session-detailed-information-bean.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{  
   "description":"schema for session items with detailed information",
   "type":"object",
   "properties":{  
      "cnt":{  
         "type":"object",
         "javaType":"java.lang.Integer"
      },
      "last":{  
         "type":"object",
         "javaType":"java.lang.Long"
      }
   }
}

Сгенерированный объект SessionDetailedInformationBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example;

import javax.annotation.Generated;
import com.google.gson.annotations.Expose;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;


/**
 * schema for session items with detailed information
 * 
 */
@Generated("org.jsonschema2pojo")
public class SessionDetailedInformationBean {

    @Expose
    private Integer cnt;
    @Expose
    private Long last;


    public Integer getCnt() {
        return cnt;
    }


    public void setCnt(Integer cnt) {
        this.cnt = cnt;
    }

    public SessionDetailedInformationBean withCnt(Integer cnt) {
        this.cnt = cnt;
        return this;
    }


    public Long getLast() {
        return last;
    }


    public void setLast(Long last) {
        this.last = last;
    }
}

Для описания всего json “IPDATA” вообще-то достаточно следующей json-схемы:

ip-data.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{  
   "id":"#ipdata",
   "type":"object",
   "properties":{  
      "IPDATA":{  
         "type":"object",
         "properties":{  
            "data":{  
               "type":"object",
               "javaType":"java.util.Map<String, java.util.Map<String, com.example.SessionDetailedInformationBean>>"
            }
         }
      }
   }
}

Но такие сложные конструкции java.util.Map<String, java.util.Map<String, com.example.SessionDetailedInformationBean>> не поддерживались в плагине версии 0.4.7.

Чтобы выйти из сложившегося затруднения, мне пришлось предпринять следующие шаги:

Шаг 1

Заменить "javaType": "java.util.Map<String, java.util.Map<String, com.example.SessionDetailedInformationBean>>" на допустимый "javaType": "java.util.Map<String, com.example.SessionInformation>"

Шаг 2

Создать класс SessionInformation, в котором, по сути, будет хранится Map<String, com.example.SessionDetailedInformationBean>.

Например, так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SessionInformation{
    private Map<String, SessionDetailedInformationBean> dataItem;

    public SessionInformation(Map<String, SessionDetailedInformationBean> dataItem) {
        this.dataItem = dataItem;
    }

    public Map<String, SessionDetailedInformationBean> getDataItem() {
        return dataItem;
    }

    public SessionInformation get(String sessionNumber) {
        return dataItem.get(sessionNumber);
    }
}

Шаг 3

Создать кастомный десериализатор:

(Советую ознакомится со статьей Gson или «Туда и Обратно», она поясняет механизмы сериализации-десериализации).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import ch.lambdaj.function.convert.Converter;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.example.SessionDetailedInformationBean;

import java.lang.reflect.Type;
import java.util.Map;

import static ch.lambdaj.Lambda.on;
import static ch.lambdaj.collection.LambdaCollections.with;

public class SessionInformationDeserializer implements JsonDeserializer<SessionInformation> {
    @Override
    public SessionInformation deserialize(JsonElement jsonElement, Type type,
                                  final JsonDeserializationContext context) throws JsonParseException {
        Map<String, SessionDetailedInformationBean> dataItemBeanMap =
                with(jsonElement.getAsJsonObject().entrySet()).map((on(Map.Entry.class).getKey().toString())).clone()
                        .convertValues(new Converter<Map.Entry<String, JsonElement>, SessionDetailedInformationBean>() {
                            @Override
                            public SessionDetailedInformationBean convert(Map.Entry<String, JsonElement> from) {
                                return context.deserialize(from.getValue(), SessionDetailedInformationBean.class);
                            }
                        });

        return new SessionInformation(dataItemBeanMap);
    }
}

Шаг 4

Использовать десериализатор по назначению, так:

1
2
3
  Gson ipDataDeserializer = new GsonBuilder().registerTypeAdapter(SessionInformation.class, new SessionInformationDeserializer ()).create();
  Map<String, SessionInformation> data = ipDataDeserializer.fromJson(response.asString(), IpData.class)
                .getIPDATA().getData();