jacksonその1

jacksonについて調べてみたときのメモです。

jacksonのリポジトリはこちら。

https://github.com/FasterXML/jackson

jacksonとは、JavaJSONライブラリ、というところまでは前提知識があります。

以前からそれとなく使っていて、Spring frameworkを使っていればフレームワークがデフォルトで使用しているはずです。

jacksonは3つの要素から構成されているようです。

他にもコミュニティによるモジュールがあるようですが、上記のモジュールはjackson開発チームで開発、メンテナンスがされているそう。

サードパーティのモジュール一覧はこちらに記載がありました。思っていたより多いです。

https://github.com/FasterXML/jackson?tab=readme-ov-file#third-party-datatype-modules

databindリポジトリチュートリアルがありました。やってみましょう。

https://github.com/FasterXML/jackson-databind

環境

java -version
openjdk version "17.0.8.1" 2023-08-24
OpenJDK Runtime Environment Temurin-17.0.8.1+1 (build 17.0.8.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.8.1+1 (build 17.0.8.1+1, mixed mode, sharing)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jackson-my-playground</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.16.0</jackson.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.10.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

read and write

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ReadWriteTest {

    @Test
    void readValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var person = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Person.class);
        assertEquals("firstName: book, lastName: store", person.toString());
    }

    @Test
    void writeValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var output = objectMapper.writeValueAsString(new Person("book", "store"));
        assertEquals("{\"firstName\":\"book\",\"lastName\":\"store\"}", output);
    }

    public static class Person {

        @SuppressWarnings("unused") // jackson use
        public Person() {

        }

        public Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String firstName;

        public String lastName;

        @Override
        public String toString() {
            return String.format("firstName: %s, lastName: %s", firstName, lastName);
        }

    }

}

jacksonによるJSONの読み込み、書き込みはObjectMapperを使います。ObjectMapperクラスはインスタンス化することで使用できます。

JSONPOJOで読み込むにはreadValueを使います。反対に書き込むにはwriteValueを使います。

書き込んだ結果をStringで貰いたいのであれば、writeValueAsStringを使います。

読み込む先のPOJOはデフォルトコンストラクタ(引数なしコンストラクタ)が必要になりました。おそらくですが、読み込み元のJSONPOJOにどうやって変換するかどうかは変更できると思うので、上記のコードのようにデフォルトでは必要ということでしょう。

collection

JSONの読み込み、書き込みはPOJOだけではありません。以下のようにCollectionのMapやListを指定することも可能です。

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class GenericCollectionTest {

    final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void readAsMap() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        Map<String, String> result = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Map.class);
        assertEquals(Map.of("firstName", "book", "lastName", "store"), result);
    }

    @Test
    void readAsList() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        List<Integer> result = objectMapper.readValue("""
                [1, 2, 3]
                """, List.class);
        assertEquals(List.of(1, 2, 3), result);
    }

    @Test
    void cantReadObjectToList() {
        assertThrows(MismatchedInputException.class, () -> objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, List.class));
    }

    @Test
    void readAsMapPersonRecord() throws JsonProcessingException {
        var people = objectMapper.readValue("""
                {
                 "NB001": {
                  "firstName": "book",
                  "lastName": "store"
                 }
                }
                """, new TypeReference<Map<Id, Person>>() {
        });
        assertEquals(Map.of(new Id("NB001"), new Person("book", "store")), people);
    }

    public record Id(String value) {
    }

    public record Person(String firstName, String lastName) {
    }

}

readAsMapPersonRecordではMapで読み込みをしつつ、valueにRecordクラスを指定しました。

このように、JSONをどのように変換するのか一つの型に収まらない場合はTypeReferenceを使えば良さそうです。