kagamihogeの日記

kagamihogeの日記です。

Spring Bootでjsonプロパティをenumにデシリアライズとvalidation

各種パターン

まず、動作確認用の適当なmainとcontrollerを作成する。

plugins {
  id 'org.springframework.boot' version '2.4.1'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}
repositories {
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  compileOnly 'org.projectlombok:lombok'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  
  implementation 'io.springfox:springfox-boot-starter:3.0.0'
  implementation 'org.springframework.boot:spring-boot-starter-validation'
}
test {
  useJUnitPlatform()
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {
    @PostMapping("/sample")
    public void sample(@RequestBody @Validated SampleRequest request) {
        System.out.println(request);
    }
}

リクエスト格納クラスのプロパティの型を色々変えて試していく。

import lombok.Data;

@Data
public class SampleRequest {
    SimpleEnum simpleEnum;
}
public enum SimpleEnum {
    on, off;
}

単純なenum

上記のとおり、特に何もせずともenumにすればバインドする。上の例だと、"simpleEnum": "on"とか"simpleEnum": "off"が通る。また、"simpleEnum": nullとかjsonプロパティ自体が無い場合はnullになる。

Optionalのenum

Optional<SimpleEnum> optSimpleEnum = Optional.empty();

この場合、"optSimpleEnum": nullを渡すとOptional.emptyになる。また、上記例はデフォルト値を入れており、jsonプロパティ自体が無い場合Optional.emptyになる。デフォルト値が無いとnullになる。

独自マッピングenum

たとえば、onは"1"でoffは"0"のように、enumとは異なる値からマッピングしたい場合。これは@JsonCreatorマッピングを定義する。

import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonCreator;

public enum ValueEnum {
    on("1"), off("0");
    
    String value;
    ValueEnum(String value) {
        this.value = value;
    }
    
    static Map<String, ValueEnum> BY_VALUE = new HashMap<>();
    static {
        for (ValueEnum e : values()) {
            BY_VALUE.put(e.value, e);
        }
    }
    
    @JsonCreator
    public static ValueEnum create(String value) {
        return BY_VALUE.getOrDefault(value, ValueEnum.off);
    }
}

上記例は、Mapにあらかじめマッピングを保持しておき、@JsonCreatorで文字列からenumへのマッピングを記述している。

なお、nulljsonプロパティ自体が無い場合はnullになる。

独自マッピングのOptionalのenum

Optional<ValueEnum> optValueEnum = Optional.empty();

この場合、nullを渡すとOptional.emptyになる。また、デフォルト値を入れているので、jsonプロパティ自体が無い場合もOptional.emptyになる。

独自マッピングenumのvalidation

妥当な値以外はvalidationエラーにしたい場合。上記例の場合、"0", "1"以外はvalidationエラーとしたい場合を考える。

@ValueEnumConstraint
ValidatedValueEnum validatedValueEnum;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = ValueEnumValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValueEnumConstraint {
    String message() default "Invalid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
import java.util.Objects;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ValueEnumValidator implements ConstraintValidator<ValueEnumConstraint, ValidatedValueEnum> {

    @Override
    public boolean isValid(ValidatedValueEnum value, ConstraintValidatorContext context) {
        return Objects.nonNull(value);
    }
}
public enum ValidatedValueEnum {
  //(ValueEnumと同一部分は省略)    
    @JsonCreator
    public static ValidatedValueEnum create(String value) {
        return BY_VALUE.getOrDefault(value, null);
    }
}

まず、@JsonCreatorで、妥当な値以外が来た場合はnullを返す。そして、validationはnullが来たらvalidationエラーにする。@JsonCreatorバインディングの後にConstraintValidatorが走る。

ただし、これはnullを許容したい場合にはvalidationエラーとなってしまう。

独自マッピングのOptionalのenumのvalidation

nullを許容するため、まずOptionalにする。

@OptValueEnumConstraint
Optional<OptValidatedValueEnum> optValidatedValueEnum = Optional.empty();

次に、enumにvalidationエラーを示すinvalidを追加する。@JsonCreatorマッピング時に、妥当な値以外はinvalidを返す。

public enum OptValidatedValueEnum {
    on("1"), off("0"), invalid("");
    //(省略) 
    @JsonCreator
    public static OptValidatedValueEnum create(String value) {
        return BY_VALUE.getOrDefault(value, invalid);
    }
}

そして、validationではinvalidが来たらエラーにし、それ以外はOKにする。

public class OptValueEnumValidator implements ConstraintValidator<OptValueEnumConstraint, Optional<OptValidatedValueEnum>> {

    @Override
    public boolean isValid(Optional<OptValidatedValueEnum> value, ConstraintValidatorContext context) {
        if (value.isPresent()) {
            return !(value.get() == invalid);
        }
        return true;
    }
}

これで、妥当な値、ここでは"0", "1", nullおよびjsonプロパティ自体が無い、以外はvalidationエラーになる。