ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jackson 으로 JSON 다루기
    Language/Java 2024. 1. 22. 06:22
    728x90
    반응형

    - 목차

     

    들어가며.

    Jackson 은 JSON 데이터를 Serialization/Deserialization 하는 대표적인 자바 라이브러리입니다.
    특히 JSON 과 POJO 사이의 변환을 손쉽게 할 수 있도록 여러 기능을 제공합니다.
    이번 글에서는 여러 사례와 실습을 통해서 Jackson 을 활용한 Parsing 방법에 대해서 알아보도록 하겠습니다.
     
     

    JDK & Jackson Compatibility.

    아래 URL 은 Jackson 의 Release 노트의 사이트의 주소입니다.
    https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.14

     

    Jackson Release 2.14

    Main Portal page for the Jackson project. Contribute to FasterXML/jackson development by creating an account on GitHub.

    github.com

     
    혹시 Java Version 과 Jackson Version 의 Compatibility 를 고민하시는 분이 계시다면 Release Note 를 확인하시는 것이 좋습니다.
    저는 JDK 11 에서 테스트를 진행할 예정이구요.
    2.14 버전은 Java 8 이상의 버전에서 호환이 가능합니다.

    Compatibility: JDK requirements
    
    JDK baseline has been raised to Java 8 for jackson-core and jackson-jr components, 
    leaving jackson-annotations the only component that only requires Java 6: 
    all other components already required Java 8.

     
    아래와 같이 build.gradle 에 2.14.2 버전의 Jackson Library 를 추가하였구요.
    jackson-core 와 jackson-databind 모듈을 사용할 예정입니다.

    dependencies {
        // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
        implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.14.2'
        // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
        implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.2'
    }

     
     

    Databind 모듈.

    Jackson 의 Databind 모듈을 통해서 JSON String 을 Tree Model 로 다룰 수 있게 됩니다.
    예를 들어, 아래의 JSON 데이터는 "Lee" 성을 가지는 가족의 간단한 관계도인데요.
    해당하는 JSON 은 아래의 그림처럼 표현될 수 있습니다.

    {
      "name" : "Andy Lee", "age" : 26,
      "parent" : [
        { "name" : "Brad Lee", "age" : 54,
          "parent" : [
            { "name" : "Tom Lee", "age" : 78, "parent" : [] },
            { "name" : "Cindy Lee", "age" : 78, "parent" : [] }        
          ]
        }
      ]
    }

     
    이러한 Tree Model 을 구현하기 위해서 제공하기 위해서 Jackson 의 ObjectMapper 클래스가 존재합니다.
    ObjectMapper 의 사용법과 JSON 변환을 코드로 구현해보겠습니다.
    올바른 JSON String 은 아래와 같이 쉽게 변환이 됩니다.
     

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonRecord = "{\n" +
                "  \"name\" : \"Andy Lee\", \"age\" : 26,\n" +
                "  \"parent\" : [\n" +
                "    { \"name\" : \"Brad Lee\", \"age\" : 54,\n" +
                "      \"parent\" : [\n" +
                "        { \"name\" : \"Tom Lee\", \"age\" : 78, \"parent\" : [] },\n" +
                "        { \"name\" : \"Cindy Lee\", \"age\" : 78, \"parent\" : [] }        \n" +
                "      ]\n" +
                "    }\n" +
                "  ]\n" +
                "}";
        JsonNode jsonNode = objectMapper.readTree(jsonRecord);
    
        System.out.println(jsonNode.get("name"));
        System.out.println(jsonNode.get("age"));
        System.out.println(jsonNode.get("parent"));
      }
    }
    "Andy Lee"
    26
    [{"name":"Brad Lee","age":54,"parent":[{"name":"Tom Lee","age":78,"parent":[]},{"name":"Cindy Lee","age":78,"parent":[]}]}]

     
     

    ObjectNode 와 ArrayNode.

    Jackson 의 Tree Model 이 다루는 데이터는 크게 세가지 타입으로 구성됩니다.
    - JsonNode
    - ObjectNode
    - ArrayNode
     
    세부적으로는 TextNode, NumericNode, BooleanNode, NullNode, MissingNode, PojoNode 등이 더 존재하는데요.
    가장 기본적은 JsonNode, ObjectNode, ArrayNode 에 대해서 먼저 설명을 이어가보겠습니다.
     
    ObjectNode 은 하나의 Record 로써의 JSON Object 에 대응되는 Jackson 의 엔티티입니다.
    아래 예시와 같이 독립적인 의미를 가지는 하나의 데이터 Record 는 Object Node 가 될 수 있습니다.

    // 인적 데이터
    { "name" : "Andy", "age" : 13 }
    
    // 거래 데이터
    { "id" : 123, "purchasedAt" : "2024-01-01 16:04:12", "productId": "P-4523"}

     
     
    ArrayNode 는 Node 들의 배열입니다.
    아래와 같이 회사의 직원들, 또는 고객의 거래목록과 같은 데이터들인 ArrayNode 로써 표현될 수 있습니다.

    // 인적 데이터
    {
      "companyName" : "westlifeCompany",
      "employees" : [
        { "name" : "Andy", "age" : 13 }, 
        { "name" : "Bod", "age" : 23 },
        { "name" : "Chris", "age" : 33 }
      ]
    }
    
    // 거래 데이터
    {
      "customerId" : "C-12345",
      "transactions" : [
        { "id" : 123, "purchasedAt" : "2024-01-01 16:04:12", "productId": "P-4523"},
        { "id" : 124, "purchasedAt" : "2024-01-01 16:05:12", "productId": "P-4524"}   
      ]
    }

     
    코드로써 예시를 들어보겠습니다.
     
    < ObjectNode Parsing >
    제가 Jackson 을 활용하여 JSON String 을 Parsing 하는 경우에 흔히 사용하는 방식인데요.
    Optional 과 JsonNode.isObject 를 적용하여 아래와 같이 ObjectNode 로 Type Casting 을 하곤합니다.
     

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    
    import java.util.Optional;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String user1 = "{ \"name\" : \"Andy\", \"age\" : 13 }";
        String transaction1 = "{ \"id\" : 123, \"purchasedAt\" : \"2024-01-01 16:04:12\", \"productId\": \"P-4523\"}";
    
        ObjectNode userObject = Optional.ofNullable(objectMapper.readTree(user1))
                .filter(JsonNode::isObject)
                .map(a -> (ObjectNode) a)
                .orElseThrow();
    
        ObjectNode transactionObject = Optional.ofNullable(objectMapper.readTree(transaction1))
                .filter(JsonNode::isObject)
                .map(a -> (ObjectNode) a)
                .orElseThrow();
    
        System.out.println(String.format("name : %s, age : %s",
                userObject.get("name"),
                userObject.get("age")));
        System.out.println(String.format("id : %s, purchasedAt : %s, productId : %s",
                transactionObject.get("id"),
                transactionObject.get("purchasedAt"),
                transactionObject.get("productId")));
      }
    }
    name : "Andy", age : 13
    id : 123, purchasedAt : "2024-01-01 16:04:12", productId : "P-4523"

     
    < ArrayNode Parsing >
     
    ArrayNode 또한 ObjectNode 와 같이 Optional 과 JsonNode.isArray 를 적용하여 Deserilization 을 시도해보겠습니다.
    아래와 같이 ArrayNode 는 forEach 메소드를 통해서 Loop 탐색이 가능하구요.
    Optional 과 조합하여 안정적으로 Transformation, Casting 등의 작업을 수행할 수 있습니다.
     

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.ArrayNode;
    
    import java.util.Optional;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String user1 = "{\n" +
                "  \"companyName\" : \"westlifeCompany\",\n" +
                "  \"employees\" : [\n" +
                "    { \"name\" : \"Andy\", \"age\" : 13 }, \n" +
                "    { \"name\" : \"Bod\", \"age\" : 23 },\n" +
                "    { \"name\" : \"Chris\", \"age\" : 33 }\n" +
                "  ]\n" +
                "}";
        String transaction1 = "{\n" +
                "  \"customerId\" : \"C-12345\",\n" +
                "  \"transactions\" : [\n" +
                "    { \"id\" : 123, \"purchasedAt\" : \"2024-01-01 16:04:12\", \"productId\": \"P-4523\"},\n" +
                "    { \"id\" : 124, \"purchasedAt\" : \"2024-01-01 16:05:12\", \"productId\": \"P-4524\"}   \n" +
                "  ]\n" +
                "}";
    
        ArrayNode userArray = Optional.ofNullable(objectMapper.readTree(user1))
                .filter(u -> u.has("employees"))
                .map(u -> u.get("employees"))
                .filter(JsonNode::isArray)
                .map(a -> (ArrayNode) a)
                .orElseThrow();
    
        ArrayNode transactionArray = Optional.ofNullable(objectMapper.readTree(transaction1))
                .filter(u -> u.has("transactions"))
                .map(u -> u.get("transactions"))
                .filter(JsonNode::isArray)
                .map(a -> (ArrayNode) a)
                .orElseThrow();
    
        userArray.forEach(userObject -> {
          System.out.println(String.format("name : %s, age : %s",
                  userObject.get("name"),
                  userObject.get("age")));
        });
    
        transactionArray.forEach(transactionObject -> {
          System.out.println(String.format("id : %s, purchasedAt : %s, productId : %s",
                  transactionObject.get("id"),
                  transactionObject.get("purchasedAt"),
                  transactionObject.get("productId")));
        });
      }
    }
    name : "Andy", age : 13
    name : "Bod", age : 23
    name : "Chris", age : 33
    id : 123, purchasedAt : "2024-01-01 16:04:12", productId : "P-4523"
    id : 124, purchasedAt : "2024-01-01 16:05:12", productId : "P-4524"

     
     

    TypeReference 를 활용하여 Primitive Type & Collection Type 변환.

    Jackson 의 TypeReference 를 사용하여 타입 변환이 가능합니다.
    특히 JVM 의 Primitive Type 와 List, Map, Set 과 같은 Collection 타입의 변환은 굉장히 자유롭습니다.
    예시를 작성해보겠습니다.
     

    Primitive Type 변환하기.

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        Integer one = objectMapper.readValue("1", new TypeReference<Integer>() {});
        System.out.println("##### " + one);
        Boolean bTrue = objectMapper.readValue("true", new TypeReference<Boolean>() {});
        System.out.println("##### " + bTrue);
        Double onePointTwo = objectMapper.readValue("1.2", new TypeReference<Double>() {});
        System.out.println("##### " + onePointTwo);
      }
    }
    ##### 1
    ##### true
    ##### 1.2

     
     

    Array JSON 문자열을 List 로 변환하기.

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.util.List;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String membersJsonStr = "[\"Andy\", \"Brad\", \"Chris\", \"Daniel\"]";
        List<String> memberList = objectMapper.readValue(membersJsonStr, new TypeReference<List<String>>(){});
        System.out.println(memberList);
      }
    }
    [Andy, Brad, Chris, Daniel]

     
     

    Object JSON 를 Map<String, String> 으로 변경하기.

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.util.Map;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String transactionJsonStr = "{ \"id\" : 123, \"purchasedAt\" : \"2024-01-01 16:04:12\", \"productId\": \"P-4523\"}";
        Map<String, String> transactionMap = objectMapper.readValue(transactionJsonStr, new TypeReference<Map<String, String>>(){});
        System.out.println(transactionMap);
      }
    }
    {id=123, purchasedAt=2024-01-01 16:04:12, productId=P-4523}

     
     

    JSON 과 POJO 변환하기.

    Pojo Class 란 Private Field 와 getter/setter Method 를 가지는 Serializable 한 Class 를 의미합니다.

    하나의 예시로써 User.class 라는 간단한 Pojo Class 를 만들어보겠습니다.

    class User implements Serializable {
      private String name;
      private Integer age;
      public void setName (String name) {this.name = name;}
      public void setAge (Integer age) {this.age = age;}
      public String getName () {return this.name;}
      public Integer getAge () {return this.age;}
    }

     

    그리고 Deserialization 도 한번 해보겠습니다.

     

    package org.example.jackson;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.Serializable;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String userJson1 = "{\"name\" :\"Andy\", \"age\" :13 }";
        String userJson2 = "{\"name\" :\"Bod\", \"age\" :23 }";
        String userJson3 = "{\"name\" :\"Chris\", \"age\" :33 }";
        User user1 = objectMapper.readValue(userJson1, new TypeReference<User>() {});
        User user2 = objectMapper.readValue(userJson2, new TypeReference<User>() {});
        User user3 = objectMapper.readValue(userJson3, new TypeReference<User>() {});
    
        System.out.println(user1);
        System.out.println(user2);
        System.out.println(user3);
      }
    }
    
    class User implements Serializable {
      private String name;
      private Integer age;
    
      public void setName(String name) {
        this.name = name;
      }
    
      public void setAge(Integer age) {
        this.age = age;
      }
    
      public String getName() {
        return this.name;
      }
    
      public Integer getAge() {
        return this.age;
      }
    
      @Override
      public String toString() {
        return String.format("this is User. name is %s, age is %s", this.name, this.age);
      }
    }
    this is User. name is Andy, age is 13
    this is User. name is Bod, age is 23
    this is User. name is Chris, age is 33


     

    Annotation 활용해보기.

    Jackson Library 에서 제공하는 Annotation 들이 존재합니다.

    특히 JSON 과 POJO 의 변환을 위해 사용되는 Annotation 들이 실무에서 많이 사용됩니다.

     

    @JsonSetter.

    @JsonSetter 는 이름 그대로 Pojo 의 Setter 를 위한 Annotation 입니다.

    Pojo 의 Field 이름과 Json 의 Key 가 서로 상이할 때에 JsonSetter Annotation 을 활용할 수 있습니다.

    User.class 의 String name Field 에 @JsonSetter("id") Annotation 을 추가하였습니다.

    @JsonSetter("id") 가 추가된 이후부터 id 라는 key 를 가진 Json 문자열만이 User.class 로 Deserialization 될 수 있습니다.

     

    package org.example.jackson;
    
    import com.fasterxml.jackson.annotation.JsonSetter;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.Serializable;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String userJson1 = "{\"id\" :\"Andy\", \"age\" :13 }";
        String userJson2 = "{\"name\" :\"Bod\", \"age\" :23 }";
        String userJson3 = "{\"nickname\" :\"Chris\", \"age\" :33 }";
        User user1 = objectMapper.readValue(userJson1, new TypeReference<User>() {});
        User user2 = null;
        User user3 = null;
        try {user2 = objectMapper.readValue(userJson2, new TypeReference<User>() {});} catch (JsonMappingException e) {System.out.println("### Fail to Parse user2");}
        try {user3 = objectMapper.readValue(userJson3, new TypeReference<User>() {});} catch (JsonMappingException e) {System.out.println("### Fail to Parse user3");}
        System.out.println(user1);
        System.out.println(user2);
        System.out.println(user3);
      }
    }
    
    class User implements Serializable {
      private String name;
      private Integer age;
    
      @JsonSetter("id")
      public void setName(String name) {
        this.name = name;
      }
    
      public void setAge(Integer age) {
        this.age = age;
      }
    
      public String getName() {
        return this.name;
      }
    
      public Integer getAge() {
        return this.age;
      }
    
      @Override
      public String toString() {
        return String.format("this is User. name is %s, age is %s", this.name, this.age);
      }
    }

     

    ### Fail to Parse user2
    ### Fail to Parse user3
    this is User. name is Andy, age is 13
    null
    null

     

     

    @JsonCreator & @JsonProperty.

    @JsonSetter 가 Pojo 의 Setter 를 위한 Annotation 이었다면, @JsonCreator 는 Constructor 를 위한 Annotation 입니다.

    바로 예시로 들어가겠습니다.

     

    package org.example.jackson;
    
    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.core.type.TypeReference;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.Serializable;
    
    public class JsonParsing {
      public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        String userJson1 = "{\"id\" :\"Andy\", \"yearsFromBirth\" :13 }";
        String userJson2 = "{\"name\" :\"Bod\", \"age\" :23 }";
        User user1 = objectMapper.readValue(userJson1, new TypeReference<User>() {});
        User user2 = objectMapper.readValue(userJson2, new TypeReference<User>() {});;
        System.out.println(user1);
        System.out.println(user2);
      }
    }
    
    class User implements Serializable {
      private String name;
      private Integer age;
    
      @JsonCreator
      User (@JsonProperty("id") String name, @JsonProperty("yearsFromBirth") Integer age) {
        this.name = name;
        this.age = age;
      }
      
      public void setName(String name) {
        this.name = name;
      }
    
      public void setAge(Integer age) {
        this.age = age;
      }
    
      public String getName() {
        return this.name;
      }
    
      public Integer getAge() {
        return this.age;
      }
    
      @Override
      public String toString() {
        return String.format("this is User. name is %s, age is %s", this.name, this.age);
      }
    }
    this is User. name is Andy, age is 13
    this is User. name is Bod, age is 23

     

    마치며.

    여기까지 간단한 Jackson Library 를 사용하는 방법에 대해 작성하였습니다.

    다음 글에서는 Json 의 Null 처리, Timestamp 처리와 같은 내용에 대해서 작성하도록 하겠습니다.

    감사합니다.

     

    반응형

    'Language > Java' 카테고리의 다른 글

    [Java NIO] NIO 는 어떻게 동작할까 ? (DMA, Kernel Space)  (0) 2024.01.31
    Java off-heap 메모리 알아보기  (0) 2023.12.22
    Java Thread Pool 알아보기  (0) 2023.09.30
    Java Future 알아보기  (0) 2023.09.30
    java Annotation 이해하기  (0) 2023.09.25
Designed by Tistory.