【问题标题】:Jackson: how do I do validation after deserialization?Jackson:反序列化后如何进行验证?
【发布时间】:2021-02-21 20:55:28
【问题描述】:

我正在使用Jackson with Kotlin对各种Kotlin进行序列化和反序列化data classes。

我的 data classes 是非常简单的对象,可以使用标准 Jackson ObjectMapper 轻松序列化和反序列化,除了我想确保完成一些后验证作为反序列化。比如我要保证Thing.someField >= 0:

data class InnerThing(
    val foo: String
);

data class Thing(
    val someField: Int,   // must not be negative
    val innerThing: InnerThing
);

实现这一点的最简单方法似乎是覆盖相关类的StdDeserializer

class ThingDeserializer : StdDeserializer<Thing>(Thing::class.java) {
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Thing {
        // Defer to the superclass to do the actual deserialization
        // DOES NOT WORK because StdDeserializer.deserialize() is abstract
        val t: Thing = super.deserialize(p, ctxt);

        if (thing.someField < 0) {
            throw RuntimeException("someField value must be >= 0");
        }

        return t;
    }
}

…但那不起作用,因为StdDeserializer.deserialize() 是抽象的。这将我引向this related question。似乎在自定义反序列化程序中遵循默认反序列化行为非常困难。

据我所知,推迟默认反序列化行为的最直接方法是创建一个完全单独的ObjectMapper (!!!),使用 that 阅读课程,然后进行后期验证。

把我引向这个……

class ThingDeserializer : StdDeserializer<Thing>(Thing::class.java) {

    // Create a whole separate ObjectMapper that doesn't override
    // the deserializer for Thing:
    private val defaultMapper = jacksonObjectMapper();

    init {
        // Need to reregister modules for things like ISO8601
        // timestamp parsing:
        defaultMapper.registerModule(JavaTimeModule());
    }    

    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Thing {
        // Use the non-overridden ObjectMapper to deserialize the object:
        val t: Thing = defaultMapper.readValue(p, Thing::class.java);

        // ... and then do validation afterwards
        if (thing.someField < 0) {
            throw RuntimeException("someField value must be >= 0");
        }

        return t;
    }
}

fun main(args: Array<String>) {
    val mapper = jacksonObjectMapper();
    mapper.registerModule(JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

    mapper.registerModule(
        SimpleModule().apply {
          addDeserializer(Thing::class.java, ThingDeserializer()) 
        }
    );

    val test: Thing = mapper.readValue("""{
      "someField": -1,
      "innerThing": { "foo": "bar" }
    }""");
}

确实有效;它会在测试用例中为不需要的 someField 值抛出一个 RuntimeException

这似乎是错误的

上述真的是一种合理的验证方式吗?

它有一种非常糟糕的代码气味。我正在创建一个全新的ObjectMapper,我必须重新注册模块以进行时间戳反序列化等操作。

问题

感觉必须有一种更理智、更少冗余的方法来反序列化 Kotlin data class(或 Java POJO),然后对对象的内容进行后验证,而无需重写完全反序列化机制。

有吗?我想不出一种简单的方法来从反序列化器中推迟ObjectMapper 的默认反序列化行为。

【问题讨论】:

  • 如果Thing 的实例不是通过反序列化创建的,它是否有意义?也许您只需将此字段类型更改为UInt 或将此检查移至init 块?
  • 谢谢@МихаилНафталь。在这种情况下,由于整个数据结构的版本控制相当复杂,其中包括嵌套元素,因此更容易验证整个结构,而不是单独的 data classes 被实例化。但是,是的……一些验证可以在这些类的 init 块中完成。
  • @oddbjorn 在下面的答案中的简单机制允许非常理智的验证(最少的代码)。一旦所需的设置到位,您只需要在您的类中使用 validate() 方法..

标签: json kotlin jackson introspection jackson-modules


【解决方案1】:

对于反序列化期间的自动验证,使用@JsonCreator 构造函数可能是最正确的方法,但如果您的数据类有很多字段,则构造函数签名最终会相当长。

通过一些努力,您可以以一种相当干净的方式完成此操作,并且仍然保持您的数据类紧凑和整洁。诀窍是通过BeanDeserializerModifierStdDelegatingDeserializerConverter 类的组合来挂钩Jackson 的反序列化过程。正确连接这些,您可以将 BeanDeserializerModifier 委托给调用 ConverterStdDelegatingDeserializer,该 Converter 在将反序列化结果对象原封不动地返回之前对其进行验证。

首先定义您的Validatable 接口:

public interface Validatable {
    void validate();
}

然后定义简单验证的Converter类:

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;

import java.util.Objects;

public class ValidatingConverter implements Converter<Object, Object> {

    private final JavaType type;

    public ValidatingConverter(JavaType type) {
        Objects.requireNonNull(this.type = type);
    }

    @Override
    public Object convert(Object value) {
        if (value instanceof Validatable validatable) {
            validatable.validate();
        }
        return value;
    }

    @Override
    public JavaType getInputType(TypeFactory typeFactory) {
        return type;
    }

    @Override
    public JavaType getOutputType(TypeFactory typeFactory) {
        return type;
    }
}

然后,ValidatingConverter 可以被StdDelegatingDeserializer 使用:

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer;
import com.fasterxml.jackson.databind.type.*;

import java.io.IOException;

public class ValidatingBeanDeserializerModifier extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(beanDesc.getType(), deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyReferenceDeserializer(DeserializationConfig config, ReferenceType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyArrayDeserializer(DeserializationConfig config, ArrayType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyCollectionLikeDeserializer(DeserializationConfig config, CollectionLikeType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyMapDeserializer(DeserializationConfig config, MapType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public JsonDeserializer<?> modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return createDelegate(type, deserializer);
    }

    @Override
    public KeyDeserializer modifyKeyDeserializer(DeserializationConfig config, JavaType type, KeyDeserializer deserializer) {
        return new KeyDeserializer() {
            @Override
            public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
                final var deserializedKey = deserializer.deserializeKey(key, ctxt);
                if (deserializedKey instanceof Validatable validatable) {
                    validatable.validate();
                }
                return deserializedKey;
            }
        };
    }

    private JsonDeserializer<?> createDelegate(JavaType type, JsonDeserializer<?> target) {
        return new StdDelegatingDeserializer<>(new ValidatingConverter(type), type, target);
    }
}

然后,ValidatingBeanDeserializerModifier 可以在 Jackson 模块中使用:

import com.fasterxml.jackson.databind.module.SimpleModule;

public class ValidationModule extends SimpleModule {
    public ValidationModule() {
        this.setDeserializerModifier(new ValidatingBeanDeserializerModifier());
    }
}

最后,我们可以通过首先创建一个Validatable 类(甚至是一条记录)来验证它是否正常工作:

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

public class Person implements Validatable {

    public String name;
    public int age;
    public Person spouse;
    public Map<String, Person> parents;
    public List<Person> children;

    @Override
    public void validate() {
        Objects.requireNonNull(name, "Person.name must be non-null");
        if (age < 0) {
            throw new IllegalArgumentException("Person.age must be >= 0");
        }
    }
}

并在测试类中运行整个设置:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.intellij.lang.annotations.Language;

public class Test {

    public static void main(String[] args) throws JsonProcessingException {
        final var objectMapper = new ObjectMapper().registerModule(new ValidationModule());
        @Language("JSON") final var json = """
                {
                  "name": "John",
                  "age": 50,
                  "spouse": {
                    "name": "Anne",
                    "age": 50
                  },
                  "parents": {
                    "father": {
                      "name": "Greg",
                      "age": 75
                    },
                    "mother": {
                      "name": "Hilda",
                      "age": 75
                    }
                  },
                  "children": [
                    {
                      "name": "Bob",
                      "age": 25
                    },
                    {
                      "name": "Wilma",
                      "age": 20
                    }
                  ]
                }""";
        final var person = objectMapper.readValue(json, Person.class);
        System.out.println(person);
    }

}

尝试将其中一个 JSON Person 对象的年龄设置为负整数,您将得到类似于以下内容的异常:com.fasterxml.jackson.databind.JsonMappingException: Person.age must be &gt;= 0 (through reference chain: Person["children"]-&gt;java.util.ArrayList[1])

使用的杰克逊版本:2.12.2

【讨论】:

    【解决方案2】:

    已经请求了一个通用的服务验证工具:

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

    虽然尚未实施。据推测,这将不仅限于基于注释,而是允许可插入的后处理(加上一个总是可以通过子类化 AnnotationIntrospector 来“伪造”注释)。

    除此之外(尚不可用),对于 POJO,如果您可以通过它传递所有属性(构造函数或工厂方法),则使用 @JsonCreator 效果很好。但不太适合验证第 3 方类型。

    【讨论】:

      猜你喜欢
      • 2020-03-30
      • 2021-12-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-28
      • 1970-01-01
      • 2011-07-10
      • 1970-01-01
      相关资源
      最近更新 更多