Hace unos días nos surgió un problema con la validación de los formularios: Si escribías un valor no numérico en los campos numéricos, saltaba un mensaje de error no controlado antes incluso de llegar al código del validador. Esto era porque Spring intentaba meter una cadena de texto en un Integer o un Float, y claro, pasaban cosas nazis y el programa te decía tal que así:

Failed to convert property value of type java.lang.String to required type java.lang.Integer for property anyo; nested exception is java.lang.NumberFormatException: For input string: "..."

Una solución era redefinir los errores de Spring en el properties:

# Errores de Spring validation
typeMismatch.java.lang.Float=Valor decimal inv\u00E1lido.
typeMismatch.java.lang.Integer=Valor entero inv\u00E1lido.

Pero claro, usar el mismo mensaje de error para todos los campos de tipo entero/decimal no es precisamente útil para saber dónde está el fallo.

Depurando, se puede ver que la excepción realmente tiene muchos “tipos”, que van del más específico al más genérico. Recogiendo el nombre más específico, que es el del campo en el que se está dando el fallo, y redefiniendo el mensaje de error en el properties, se puede hacer que el mensaje sea más descriptivo del error:

Se ve muy pequeño pero si haceis zoom se ve ^^U
typeMismatch.objeto.anyo1=El campo A\u00F1o debe tener un valor entero.

Así, el error está controlado cuando se mete un valor con un formato equivocado, y si el formato es correcto pasará al validador y se realizarán las comprobaciones que se quieran hacer sobre él.

Ahora bien, si ese campo en concreto es obligatorio no se puede dejar ahí la cosa: El error de «campo obligatorio» saltará tanto si dejamos el campo vacío como si escribimos un valor con formato incorrecto (estamos hablando de campos de tipo numérico, no nos olvidemos). Porque al haber saltado un error en la validación de Spring, anterior al código del validador, en el objeto a validar no se cargará ningún valor, y a efectos sería como si hubiéramos dejado el campo en blanco. Solo que no lo hemos dejado en blanco y antes ha saltado la excepción de formato incorrecto, así que nos van a saltar dos mensajes de error: De formato incorrecto y de campo obligatorio vacío. Y claro, eso está mal.

En ese caso hay que pensar en quitar las validaciones del validador y hacerlas todas por anotaciones, para que sea Spring el que controle errores de formato, rango, número de decimales y demás:

@Min(value=0, message=”{error.formato.anyo.menordemilnovecientos}”)
private Integer anyo;

Y los mensajes de error personalizados – el “error.formato.anyo.menordemilnovecientos” – se declararán en un properties aparte, que se debe llamar “ValidationMessages”.

error.formato.anyo.menordemilnovecientos=El A\u00F1o debe ser mayor de 1900.

Si todas las validaciones que se necesita hacer son de tipo, de rango, y en general cosas que conciernen solo a cada campo por separado, eliminamos el validador personalizado, dejamos las anotaciones, y nos despreocupamos porque ya está todo correcto. Pero si tenemos que hacer alguna validación más, por ejemplo que en un rango de fechas el año inicial sea menor que el final, o en general que el valor de unos campos dependa del valor de otros, no podemos borrar el validador porque esas comprobaciones no se pueden eliminar. Pero si usamos un validador personalizado, las validaciones por anotaciones no se ejecutan, y estamos otra vez como al principio.

No hay que preocuparse porque puede modificarse el código para que se ejecuten las validaciones por anotaciones y los validadores personalizados, y los cambios no son demasiado complicados.

En primer lugar, en el validador quitaremos todas las comprobaciones que vayan a hacerse por anotaciones – si aún no se ha hecho –, porque no queremos mensaje duplicados. También añadiremos la anotación @Component a la clase – si no lo teníais ya –.

@Component
public class ObjetoValidation implements Validator {

En el controlador, quitaremos el código que añade el validador en InitBinder

@InitBinder
Protected void initBinder(final WebDataBinder binder){
binder.setValidator(validator);
}

El validador pasará a ser un atributo:

@Autowired
private ObjetoValidation validator;

En el método del controlador posterior a la validación, dejamos la anotación @Valid delante del argumento a validar:

public ModelAndView guardarObjeto(@Valid @ModelAttribute("objeto") Objeto objeto,BindingResult errors,…)

Y añadimos, al principio del método:

Validator.validate(objeto, result);

Con estos cambios, al ejecutar, deberían ejecutarse tanto las validaciones de las anotaciones como las del validador personalizado, y ya tenemos controlados todos los errores 😀

Como nota aparte, y lo añado porque a mi se me pasó hacerlo, si el objeto a validar tiene atributos que son a su vez tipos declarados en la aplicación, habrá que añadirles la anotación @Valid para que se validen:

@Valid
private Collection<ObjetoEnObjeto> coleccionObjetosEnObjeto = new ArrayList< ObjetoEnObjeto >();

Si no se añade la anotación, al hacer las validaciones, Spring ignorará ese atributo y volveremos a tener errores sin controlar si metemos un valor incorrecto.

…………………………………………………………………………………..

Espero que este post pueda ayudar a alguien – a mi desde luego me molestó la poca información que he encontrado al respecto, y lo dispersa que estaba… Por no mencionar que estaba toda en inglés –. Y si no lo ha hecho, pues como siempre, aquí tenéis un gato para compensar por las molestias.

enhanced-buzz-25879-1337003705-5