2

There is a kotlin class with the following structure.

data class Person(
    @field:Length(max = 5)
    val name: String,
    val phones: List<Phone>
)

data class Phone(
    @field:Length(max = 10)
    val number: String
)

When converting the json string through objectMapper, I want to receive all the violation values.

ex) JSON object is not valid. Reasons (3) name length must be 5, number length must be 10, ...

@Test
fun test() {
    val json = """
        {
            "name": "name",
            "phones": [
                { "number": "1234567890123456" },
                { "number": "1234567890123456" }
            ]
        }
    """.trimIndent()
    try {
        objectMapper.readValue(json, Person::class.java)
    } catch (ex: ConstraintViolationException) {
        val violations = ex.constraintViolations
        println(violations.size) // expected size = 3
    }
}

However, the above code fails to catch the exception and causes the exception below.

com.fasterxml.jackson.databind.JsonMappingException: JSON object is not valid. Reasons (1): {"bean":"Phone","property":"number","value":"1234567890123456","message": "..."},  (through reference chain: Person["phones"]->java.util.ArrayList[0])

Looking at the reason, those wrapped in a list do not throw ConstructionViolationException, but throw JsonMappingException.

below dependencies

plugins {
    id("org.springframework.boot") version "2.4.3"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.4.30"
    kotlin("plugin.spring") version "1.4.30"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}
class BeanValidationDeserializer(base: BeanDeserializerBase?) : BeanDeserializer(base) {

    private val logger = LoggerFactory.getLogger(this::class.java)
    private val validator = Validation.buildDefaultValidatorFactory().validator

    override fun deserialize(parser: JsonParser?, ctxt: DeserializationContext?): Any {
        val instance = super.deserialize(parser, ctxt)
        validate(instance)
        return instance
    }

    private fun validate(instance: Any) {
        val violations = validator.validate(instance)
        if (violations.isNotEmpty()) {
            val message = StringBuilder()
            message.append("JSON object is not valid. Reasons (").append(violations.size).append("): ")
            for (violation in violations) {
                message.append("{\"bean\":\"${violation.rootBeanClass.name}\",")
                    .append("\"property\":\"${violation.propertyPath}\",")
                    .append("\"value\":\"${violation.invalidValue}\",")
                    .append("\"message\": \"${violation.message}\"}")
                    .append(", ")
            }
            logger.warn(message.toString())
            throw ConstraintViolationException(message.toString(), violations)
        }
    }
}

@Bean
fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
    .featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION)
    .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .modules(ParameterNamesModule(), JavaTimeModule(), Jdk8Module(), KotlinModule(), customValidationModule())
    .build()

@Bean
fun customValidationModule(): SimpleModule {
    val validationModule = SimpleModule()
    validationModule.setDeserializerModifier(object : BeanDeserializerModifier() {
        override fun modifyDeserializer(
            config: DeserializationConfig?,
            beanDesc: BeanDescription?,
            deserializer: JsonDeserializer<*>?
        ): JsonDeserializer<*>? {
            return if (deserializer is BeanDeserializer) {
                BeanValidationDeserializer(deserializer as BeanDeserializer?)
            } else deserializer
        }
    })
    return validationModule
}

I'm not sure how to do it. I ask for your help.

  • please attach all your question related maven/gradle dependencies – Naor Tedgi Oct 29 '21 at 06:11
  • I'm currently using the below version - org.springframework.boot "2.4.3" - jackson "2.11.4" – counter_punch_ Oct 29 '21 at 06:28
  • 1
    `ObjectMapper#readValue` (or any other method) will never throw `ConstraintViolationException` as it is beyond Jackson's domain. You'll have to write your own custom ObjectMapper (that extends or decorates original one) and perform validation against parsed object programmatically, and throw ConstraintViolationException if needed – Nikolai Shevchenko Oct 29 '21 at 07:44
  • `JsonMappingException` is produced before validation starts. So you have to fix it first. The reason of this error should be explained in `"message": "..."`. Was there a text that you replaced with ellipsis? – I.G. Oct 29 '21 at 10:12

2 Answers2

2

I would say an easier and more maintainable way would be to define a JSON Schema.

After that is in place, you can use one of the two json validation libraries mentioned here (https://json-schema.org/implementations.html#validator-kotlin) to validate your json.

rbs
  • 21
  • 2
0

The answer by @rbs is good but requires the overhead of creating json schema per json you want to validate.

Seems like object mapper can be configured not to wrap the exceptions it throws with JsonMappingException. - https://github.com/FasterXML/jackson-databind/issues/2033

All you need to do is to disable WRAP_EXCEPTIONS feature for the objectmapper

Note you cant choose a specific exception type, it will not wrap ALL the exceptions.

yotam hadas
  • 702
  • 3
  • 14