Wednesday, October 12, 2016

Jackson - Json Formatter

  1. Basic Idea about Jackson’s classes and other important things:
    1. class ObjectMapper:
      This mapper (or, data binder, or codec) provides functionality for converting between Java objects (instances of JDK provided core classes, beans), and matching JSON constructs. It will use instances of JsonParser and JsonGenerator for implementing actual reading/writing of JSON.
      <e.g.> mapper.writeValueAsString(object)
                 mapper.readValue(json, Object.class)
    2. class JsonGenerator and JsonParser:
      JsonGenerator is responsible to write json text, while JsonParser is responsible to parse the ċ°ħjson text.
    3. class StdSerializer and StdDeserializer:
      When JsonGenerator wants to generate json text, it needs to call all kinds of serializers to convert objects to corresponding json text. StdSerializer is the base class used by all standard serializers, and can also be used for custom serializers.
      StdDeserializer plays similar role in deserializing process.
    4. class JsonNode:
      Jackson has a built-in tree model which can be used to represent a JSON object. Jackson's tree model is useful if you don't know how the JSON you will receive looks, or if you for some reason cannot (or just don't want to) create a class to represent it.
      JSON tree model is represented by JsonNode.
    5. Jackson Annotations:
      The Jackson JSON toolkit contains a set of Java annotations which you can use to influence how JSON is read into objects, or what JSON is generated from the objects.
      <e.g.,> @JsonProperty, @JsonCreator
  2. Serialization: convert an object to a Json text:
    ———————————————————————————————————
    ObjectMapper mapper = new ObjectMapper();
    // write json to file: mapper.writeValue(File resultFile, Object value);
    // generate a string: String json = mapper.writeValueAsString(value);
    // write json to stream: mapper.writeValue(OutputStream out, Object value);
    ———————————————————————————————————
    Let first take a look at a simplified process to serialize a standard java class.

    For a user-defined class, how can JsonGenerator knows which field should be included in json text, and what should be its external property name (the key), there are four ways: 
    1. Use annotation @JsonProperty(“key_name”):@JsonPropery (also indicates that property is to be included) is used to indicate external property name, name used in data format (JSON or one of other supported data formats)
      The annotation should be put in front of fields.e.g.,
      public class PersonValue {
          @JsonProperty(“id”)
          public long   personId = 0;

          @JsonProperty(“name”)
          public String name = null;
      }
      //The Json text will be {“id”:0, “name”:null}
    2. Use annotation @JsonGetter(“key_name”):
      @JsonGetter is used to tell Jackson that a certain field value should be obtained from calling a getter method instead of via direct field access.
      The annotation put in front of a method. The method’s return value is the value of json.e.g.,
      public class PersonValue {
          @JsonProperty(“id”)
          public long   personId = 0;
          public String name = null;

          @JsonGetter(“name”)
          public String getName(){
              return this.name;
          }
      }
      //The Json text will be {“id”:0, “name”:null}
      [Attention] @JsonProperty and @JsonGetter can be used together. 
    3. Use annotation @JsonValue:
      The Jackson annotation @JsonValue tells Jackson that Jackson should not attempt to serialize the object itself, but rather call a method on the object which serializes the object to a JSON string.Deserialization: convert an Json text to an object:
      e.g.,
      public class PersonValue {

          public long   personId = 0;
          public String name = null;

          @JsonValue
          public String toJson(){
              return “{\”id\”:” + this.personId + "," + “\”name\”:” +this.name +”}";
          }
      }
    4. Self-define serializer
      e.g.,
      @JsonSerialize(using = PersonValueSerializer.class)
      public class PersonValue {
          public long   personId = 0;
          public String name = null;
      }

      class PersonValueSerializer extends StdSerializer<PersonValue>{
          public PersonValueSerializer(){
              super(PersonValue.class, true);
          }

          @Override
          public void serialize(PersonValue person, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonGenerationException {
              //method-1: write person as an json array
              jsonGenerator.writeStartArray();
              jsonGenerator.writeNumber(this.personId);
              jsonGenerator.writeString(this.name);
              jsonGenerator.writeEndArray();

              //method-2: write person as an json object        jsonGenerator.writeStartObject();
              jsonGenerator.writeNumberField(“id”, this.personId);
              jsonGenerator.writeStringField(“name”, this.name);
              jsonGenerator.writeEndObject();
          }
      }
      When ObjectMapper calls the function “writeValueAsString(person)”, it knows it should use the PersonValueSerializer to serialize the object.
      If we use the method-1, the json text will be like “[271, “Hongkai Wu"]”.
      If we use the method-2, the json text will be like “{“id”:271, “name”:”Hongkai Wu"}
  3. Deserialization: convert a Json text to an object:
    Similarly, let's take a look at simplified process of deserialization

    There are three ways to construct a custom defined class' object from Json text
    1. Use annotation @JsonCreator and @JsonProperty in a constructor
      @JsonCreator: is used to help Jackson construct the Java object from Json text.
      The annotation should be put in front of a class constructor.e.g.,
      public class PersonValue {

          public long   personId = 0;
          public String name = null;

          @JsonCreator
          public PersonValue(@JsonProperty(“id”) id,
                                         @JsonProperty(“name”) name){
               this.id = id;
               this.name = name;
          }
      }
      When deserializing the json text, ObjectMapper knows it should call the constructor with the annotation @JsonCreator. It will map the @JsonProperty with the field name and read the field value.
    2. Use annotation @JsonCreator and @JsonSetter(“key_name”)
      @JsonSettoer is used to tell Jackson that is should match the name of this setter method to a property name in the JSON data, when reading JSON into objects.
      The annotation should be put in front of a method
      e.g.,
      public class PersonValue {

          public long   personId = 0;
          public String name = null;

          @JsonCreator
          public PersonValue(    //@JsonProperty(“id”) id,
                                                @JsonProperty(“name”) name){
              this.id = id;
              this.name = name;
          }

          @JsonSetter(“id”)
          public void setID(long id){
               this.id = 0;
          }
      }
      When deserializing the json text, ObjectMapper knows it should call the constructor and the function with the annotation @JsonSetter to construct the object. One thing worthy attention here is that when @JsonProperty(“id") in the constructor and @JsonSetter(“id") exist simultaneously, as shown in the above example, the ObjectMapper will ignore the @JsonSetter and use @JsonProperty to construct the field value.
    3. Custom Defined Deserializer
      For a custom defined deserializer, the deserialization process is shown in the graph below:

      important API:
          jsonParser.nextToken(): return the next JsonToken object
          jsonParser.getCurrentName(): get the field name
          jsonParser.readValueAs(ObjClass.class): read the value

          jsonToken.equals(JsonToken.FIELD_NAME): return true if the token is a field
      Let's read the following code example to understand the process:
      e.g.,
      //@JsonSerialize(using = PersonValueSerializer.class)
      @JsonDeserialize(using = PersonValueDeserializer.class)
      public class PersonValue {
          @JsonProperty(“id”) public long personId = 0;
          @JsonProperty(“name”) public String name = null;
          @JsonProperty(“friends”) public List<String> friends = new ArrayList<>();
      }

      class PersonValueDeserializer extends Deserializer<PersonValue>{
          public PersonValueDeserializer(){
              super(PersonValue.class);
          }

          @Override
          public PersonValue deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
               PersonValue p = new PersonValue();
               //build an empty object, fill the fields later using JsonParser

               while(!jsonParser.isClosed()){
                   JsonToken token = jsonParser.nextToken();
                   //JsonParser is like an iterator. The deserializing process is to read the tokens one by one.            

                   if(token.equals(JsonToken.FIELD_NAME)){
                       String fieldName = jsonParser.getCurrentName();
                       //now we know the field name, next we need to know the field value
                       switch(fieldName){
                           case “id”:
                               jsonParser.nextToken();      //now we move the iterator to the field value
                               p.id = jsonParser.readValueAs(Long.class);
                               break;
                           case “name”:
                               jsonParser.nextToken();
                               p.name = jsonParser.readValueAs(String.class);
                               break;
                           case “friends”:
                               //the value of this field is an array, so the first token is JsonToken.START_ARRAY "[" 
                              jsonParser.nextToken();
                              while(jsonParser.nextToken() != JsonToken.END_ARRAY){
                                  //the last token for the field “friends” should be JsonToken.END_ARRAY ]
                                  p.friends.add(jsonParser.readValueAs(String.class);
                              }
                              //read all friends name until jsonParser’s token is END_ARRAY
                              break;
                       }                            
               }
               return p;
          }
      }
  4. Other important annotations:
    1. @JsonInclude:
      tells Jackson only to include properties under certain circumstances.
      put in front of class defn.
      e.g.,
      @JsonInclude(JsonInclude.Include.NON_EMPTY)
      public class PersonValue {
             …...
      }
    2. @JsonIgnore:
      is used to tell Jackson to ignore a certain property (field) of a Java object.
      put in front of fields.e.g.,
      public class PersonValue {
          @JsonIgnore
          public long   personId = 0;

          public String name = null;
      }
  5. Jackson TreeModel

1 comment:

  1. You have worked to perfection on this article. Thanks for taking the time to post search valuable information. I Recommendation this. JSON Formatter Online

    ReplyDelete