Tips for validating JSON requests

General

This post contains some useful(or maybe not so useful) information about json validation with Spring Boot and Kotlin. One example(about using real json property names) is also applicable to Java.

We will have a simple controller for adding items and response object for returning our API errors.

@RestController
class ItemController {

    @PostMapping("/items")
    fun addItem(@RequestBody @Valid item: Item) {
        println("Item added: $item")
    }
}
class ValidationError(val violations: List<Violation>)
class Violation(val field: String, val message: String)

You can see all code in the repository.


Apply right use-site annotation

When you try to validate some constructor parameter, for example:

data class Item(
        val name: String
)

You need to use proper Annotation Use-site Target, because of the default behavior:

the target is chosen according to the @Target annotation of the annotation being used)

data class Item(
        @get:NotBlank
        val name: String
)

See also: SO answer.


Handling nulls

Validation can only occur after the object has been created by Jackson. So, if you have some not nullable type and the request wouldn’t contain that field, you will have to deal with another exception: MissingKotlinParameterException. However, it derives from JsonMappingException and you can map it to the object you need. See example:

@ExceptionHandler(value = [MissingKotlinParameterException::class])
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleMissingKotlinParameter(exception: MissingKotlinParameterException): ValidationError {
    val fieldName = exception.path.joinToString(separator = ".") { it.fieldName }
    val violation = Violation(fieldName, "must not be null")
    return ValidationError(listOf(violation))
}

Use real json property name in returned violations

Info

Note that you will need hibernate-validator with version 6.1 at least. This version is currently in alpha.

implementation("org.hibernate.validator:hibernate-validator:6.1.0.Alpha5")

Imagine that you use @JsonProperty annotation or Jackson’s property naming strategy to customize your json field names.

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE
data class Item(
        @get:NotBlank
        val name: String,
        val description: String,
        @get:NotBlank
        @JsonProperty("completelyDifferentName")
        val someField: String,
        @get:NotBlank
        val anotherField: String
) {
    @NotBlank
    var anotherOne: String? = null
}

When you try to map validator’s violations you will receive a real field name, from your object. That field name may have no meaning for you api client.

Request:

{
    "description": "1",
    "completelyDifferentName": "",
    "another_field": "",
    "name": ""
}

Response:

{
  "violations": [
    {
      "field": "anotherField",
      "message": "must not be blank"
    },
    {
      "field": "name",
      "message": "must not be blank"
    },
    {
      "field": "someField",
      "message": "must not be blank"
    },
    {
      "field": "anotherOne",
      "message": "must not be blank"
    }
  ]
}

Luckily, PR have been recently merged into hibernate-validator master. You can use it with Spring with some configuration.

First, we need to write our own PropertyNameProvider

class JacksonPropertyNodeNameProvider(private val objectMapper: ObjectMapper) : PropertyNodeNameProvider {

    override fun getName(property: Property): String {
        return if (property is JavaBeanProperty) {
            getJavaBeanPropertyName(property)
        } else getDefaultName(property)
    }

    private fun getJavaBeanPropertyName(property: JavaBeanProperty): String {
        val type = objectMapper.constructType(property.declaringClass)
        val description: BeanDescription = objectMapper.serializationConfig.introspect(type)
        val jsonName = description.findProperties()
                .filter { it.internalName == property.name }
                .map(BeanPropertyDefinition::getName)
                .firstOrNull()
        return jsonName ?: property.name
    }

    private fun getDefaultName(property: Property): String {
        return property.name
    }
}

See here for more detailed explanation.

Then, we need to add this provider to ValidatorFactoryBean. I don’t know if there is a better way to do this, but I’m just extending the base class here:

class ValidatorFactoryBean(private val objectMapper: ObjectMapper) : LocalValidatorFactoryBean() {

    override fun getClockProvider(): ClockProvider {
        return DefaultClockProvider.INSTANCE
    }

    override fun postProcessConfiguration(configuration: javax.validation.Configuration<*>) {
        if (configuration is HibernateValidatorConfiguration) {
            configuration.propertyNodeNameProvider(JacksonPropertyNodeNameProvider(objectMapper))
        }
        super.postProcessConfiguration(configuration)
    }

    override fun getRejectedValue(field: String, violation: ConstraintViolation<Any>, bindingResult: BindingResult): Any? {
        return violation.invalidValue
    }
}

Then, just inject your own ValidatorFactoryBean into configuration:

@Configuration
class ValidationConfiguration(private val objectMapper: ObjectMapper) {
    @Bean
    fun validatorFactory(): LocalValidatorFactoryBean {
        return ValidatorFactoryBean(objectMapper)
    }
}

After that, you can return actual json field names:

{
  "violations": [
    {
      "field": "another_field",
      "message": "must not be blank"
    },
    {
      "field": "another_one",
      "message": "must not be blank"
    },
    {
      "field": "name",
      "message": "must not be blank"
    },
    {
      "field": "completelyDifferentName",
      "message": "must not be blank"
    }
  ]
}