JSON 스키마가 동적인 경우 Jackson으로 매핑하기 (동적 스키마)

유알·2024년 5월 21일
0

상황 요약

외부 api를 호출할 때, 다음과 같은 상황이 발생했었다.

        String successResponse1 = """
                {
                    "success": true,
                    "result": {
                        "data1": "value1",
                        "data2": "value2"
                    }
                }
                """;
        String failureResponse = """
                {
                    "success": false,
                    "code": "1",
                    "result": {
                        "msg": "some error message"
                    }
                }
                """;

저 result안의 값은 api마다 다른 스키마로 채워서 준다. 그리고 만약 success가 false일 때는 result안이 msg만 있는 필드로 채워져서 온다.
그니까, 성공 여부에 따라 다른 형태의 json이 오는 셈이다.

이러한 경우 objectMapper를 통해 객체에 매핑하게 되면, 실패했을 경우에 제대로 매핑되지 않은 문제가 있었다.

이를 제너릭과, JsonSubtype을 활용하여 해결하였다,

코드

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

/**
 * 공통 응답 객체 정의<br>
 * @implNote json - 객체간 변환 가능하도록 설계됨
 * @param <T> payload type
 */
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "success")
@JsonSubTypes({
        @JsonSubTypes.Type(value = SuccessResponseBody.class, name = "true"),
        @JsonSubTypes.Type(value = FailedResponseBody.class, name = "false")
})
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public sealed abstract class ResponseBody<T> permits SuccessResponseBody, FailedResponseBody{
    private String code;

    protected ResponseBody() {
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}
import com.fasterxml.jackson.annotation.JsonTypeName;

@JsonTypeName("false")
public final class FailedResponseBody<T> extends ResponseBody<T> {
    private ErrorPayload result;

    protected FailedResponseBody() {
    }

    public FailedResponseBody(String code, String msg) {
        super.setCode(code);
        this.result = new ErrorPayload(msg);
    }

    public ErrorPayload getResult() {
        return result;
    }


    public static class ErrorPayload {
        private String msg;

        public ErrorPayload(String msg) {
            this.msg = msg;
        }

        protected ErrorPayload() {
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
}
import com.fasterxml.jackson.annotation.JsonTypeName;

@JsonTypeName("true")
public final class SuccessResponseBody<T> extends ResponseBody<T>{
    private T result;

    protected SuccessResponseBody() {
        result = null;
    }

    public SuccessResponseBody(T result) {
        this.result = result;
    }

    public T getResult() {
        return result;
    }
}

테스트 코드

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.changppo.commons.*;
import org.changppo.commons.old.Response;
import org.changppo.commons.old.Result;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;

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

public class DynamicMappingTest {
    ObjectMapper objectMapper = new ObjectMapper();

    @ParameterizedTest
    @DisplayName("기존 방식대로 하면, 예외 발생함")
    @MethodSource("dynamicJsonResponseWithPayloadFormat1")
    void failed(TestCase<String, Class<?>> testCase) throws JsonProcessingException {
        String json = testCase.input();
        Assertions.assertThrows(Exception.class, () -> {
            Response response = objectMapper.readValue(json, Response.class);
            Result result = response.getResult();
            System.out.println(result.getClass().getSimpleName());
        });
    }


    @ParameterizedTest
    @DisplayName("payload format 1")
    @MethodSource("dynamicJsonResponseWithPayloadFormat1")
    void advancedTestDynamicMapping(TestCase<String, Consumer<Object>> testCase) throws JsonProcessingException {

        String json = testCase.input();
        TypeReference<ResponseBody<PayloadFormat1>> typeReference = new TypeReference<>() {
        };
        ResponseBody<PayloadFormat1> payloadFormatResponseBody = objectMapper.readValue(json, typeReference);

        // Then
        testCase.expected().accept(payloadFormatResponseBody);
        String s = objectMapper.writeValueAsString(payloadFormatResponseBody);
        System.out.println(s);
    }

    @ParameterizedTest
    @DisplayName("payload format 2")
    @MethodSource("dynamicJsonResponseWithPayloadFormat2")
    void advancedTestDynamicMapping2(TestCase<String, Consumer<Object>> testCase) throws JsonProcessingException {

        String json = testCase.input();
        TypeReference<ResponseBody<PayloadFormat2>> typeReference = new TypeReference<>() {
        };
        ResponseBody<PayloadFormat2> payloadFormatResponseBody = objectMapper.readValue(json, typeReference);

        // Then
        testCase.expected().accept(payloadFormatResponseBody);
        String s = objectMapper.writeValueAsString(payloadFormatResponseBody);
        System.out.println(s);
    }





    public record TestCase<I, E>(
            I input,
            E expected
    ) {
    }

    record PayloadFormat1(
            String data1,
            String data2
    ) {
    }

    record PayloadFormat2(
            List<PayloadFormat1> data
    ) {
    }

    private static Stream<TestCase<String, Consumer<Object>>> dynamicJsonResponseWithPayloadFormat1() {

        String successResponse1 = """
                {
                    "success": true,
                    "result": {
                        "data1": "value1",
                        "data2": "value2"
                    }
                }
                """;

        Consumer<Object> consumer1 = o -> {
            if (!(o instanceof SuccessResponseBody successResponseBody)) {
                fail();
                return;
            }
            Object result = successResponseBody.getResult();
            if (!(result instanceof PayloadFormat1 payloadFormat1)) {
                fail();
                return;
            }
            assertEquals("value1", payloadFormat1.data1());
            assertEquals("value2", payloadFormat1.data2());
        };
        String successResponse2 = """
                {
                    "success": true,
                    "code": "0",
                    "result": {
                        "data2": "value2"
                    }
                }
                """;
        Consumer<Object> consumer2 = o -> {
            if (!(o instanceof SuccessResponseBody successResponseBody)) {
                fail();
                return;
            }
            Object result = successResponseBody.getResult();
            if (!(result instanceof PayloadFormat1 payloadFormat1)) {
                fail();
                return;
            }
            assertEquals("value2", payloadFormat1.data2());
        };
        String failureResponse = """
                {
                    "success": false,
                    "code": "1",
                    "result": {
                        "msg": "some error message"
                    }
                }
                """;
        Consumer<Object> consumer3 = o -> {
            if (!(o instanceof FailedResponseBody failedResponseBody)) {
                fail();
                return;
            }
            assertEquals("1", failedResponseBody.getCode());
            FailedResponseBody.ErrorPayload result = failedResponseBody.getResult();
            assertEquals("some error message", result.getMsg());
        };

        return Stream.of(
                new TestCase<>(successResponse1, consumer1),
                new TestCase<>(successResponse2, consumer2),
                new TestCase<>(failureResponse, consumer3)
        );
    }

    private static Stream<TestCase<String, Consumer<Object>>> dynamicJsonResponseWithPayloadFormat2() {
        String successResponse = """
                {
                    "success": true,
                    "result": {
                        "data": [
                            {
                                "data1": "value1",
                                "data2": "value2"
                            },
                            {
                                "data1": "value3",
                                "data2": "value4"
                            }
                        ]
                    }
                }
                """;
        Consumer<Object> consumer = o -> {
            if (!(o instanceof SuccessResponseBody successResponseBody)) {
                fail();
                return;
            }
            Object result = successResponseBody.getResult();
            if (!(result instanceof PayloadFormat2 payloadFormat2)) {
                fail();
                return;
            }
            List<PayloadFormat1> data = payloadFormat2.data();
            assertEquals(2, data.size());
            assertEquals("value1", data.get(0).data1());
            assertEquals("value2", data.get(0).data2());
            assertEquals("value3", data.get(1).data1());
            assertEquals("value4", data.get(1).data2());
        };

        String failureResponse = """
                {
                    "success": false,
                    "code": "1",
                    "result": {
                        "msg": "some error message"
                    }
                }
                """;
        Consumer<Object> consumer2 = o -> {
            if (!(o instanceof FailedResponseBody failedResponseBody)) {
                fail();
                return;
            }
            assertEquals("1", failedResponseBody.getCode());
            FailedResponseBody.ErrorPayload result = failedResponseBody.getResult();
            assertEquals("some error message", result.getMsg());
        };

        return Stream.of(
                new TestCase<>(successResponse, consumer),
                new TestCase<>(failureResponse, consumer2)
        );

    }


}
profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글