CodeGym /Cursos /Spring REST & MVC /Campos desconocidos en JSON: strict/ tolerant

Campos desconocidos en JSON: strict/ tolerant

Spring REST & MVC
Nivel 12, Lección 3
Disponible

1. Campos desconocidos

Si alguna vez te has sorprendido pensando «bueno, si el JSON es válido, entonces todo está bien», hoy tendrás que dejar ir esa idea con cuidado. En una API HTTP, la corrección se divide al menos en dos capas: la sintaxis (el JSON se puede analizar) y el contrato (el JSON se corresponde con el modelo esperado). Un campo desconocido es como una carta sin errores, pero dirigida a otra persona: el cartero, por supuesto, hizo su trabajo, pero la carta igualmente no sabe dónde colocarla.

Malformed JSON — es cuando el JSON no se puede leer en absoluto: falta una coma, una comilla, la estructura está rota. Tal solicitud falla «en la entrada», porque el analizador JSON no puede construir el árbol.

Unknown field — es cuando el JSON se lee perfectamente, pero en su interior hay una clave que el DTO no espera. Es decir, la estructura es correcta, pero has añadido «improvisación» al contrato.

Tomemos nuestra habitual solicitud de creación de tarea. Supongamos que la entrada canónica se ve más o menos así:

{
  "title": "Fix pagination bug",
  "description": "Reproducible on page=2"
}

Y el cliente (o un probador, o «tú mismo en dos semanas») envió esto:

{
  "title": "Fix pagination bug",
  "description": "Reproducible on page=2",
  "createdAt": "2026-03-21T10:15:00Z"
}

El JSON es correcto. Pero createdAt — no es un campo del DTO de entrada (y generalmente suele ser un valor gestionado por el servidor). A partir de aquí comienza lo interesante: el servidor debe decidir si considerar esto como un error o simplemente «hacer como si no hubiera pasado nada».

Esta es una tarea diferente a ocultar propiedades conocidas del DTO mediante @JsonIgnore o @JsonIgnoreProperties({"..."}). Allí la propiedad del tipo ya existe y decidimos si debe participar en el contrato JSON. Aquí es al revés: el cliente trajo una clave que no existe en el DTO en absoluto.

2. Dónde se detectan los campos desconocidos

Para no adivinar con los posos del café, es útil ver en qué lugar se produce la comprobación de campos desconocidos. En MVC esto ocurre en la etapa de lectura del request body — allí donde trabaja HttpMessageConverter (para JSON suele ser un convertidor basado en Jackson). Y esto significa que los campos desconocidos no son «lógica del servicio» ni «error de regla de negocio». Es una cuestión de deserialización del JSON de entrada.

Resulta útil tener en mente este esquema (muy aproximado, pero honesto):

flowchart TD
  A["Cuerpo de la solicitud HTTP: JSON"] --> B["HttpMessageConverter"]
  B --> C["Jackson: JSON -> DTO"]
  C --> D["Método del controlador"]
  D --> E["Capa de servicio"]

En modo estricto, en la etapa Jackson: JSON -> DTO ocurre aproximadamente lo siguiente: Jackson ve la clave createdAt, busca el campo/accessor/componente correspondiente del record, no lo encuentra y dice: «Lo siento, yo no pedí esto». Como resultado, Spring responde al cliente con un error, normalmente con el estado 400 Bad Request.

Para ver el momento del fallo, tomemos un fragmento local de DTO que solo espera title. Aquí necesitamos precisamente el tipo destino concreto: un campo desconocido se convierte en error no «por sí mismo», sino en el momento en que Jackson intenta ensamblar un DTO comprensible a partir del JSON.

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

// Fragmento local de DTO: para esta entrada Jackson espera solo el campo title
record StrictTaskCreateRequestExample(String title) {}

@RestController // Controlador de demostración para ilustrar el momento donde falla la deserialización
public class DebugController {

    @PostMapping("/api/v1/debug/tasks") // El endpoint solo se ejecutará si el JSON se ha podido convertir a DTO
    public String create(@RequestBody StrictTaskCreateRequestExample request) {
        System.out.println("Controller method executed"); // En modo estricto con clave desconocida, esta línea no se alcanza
        return "ok";
    }
}

El ejemplo sigue siendo mínimo: en una API real aquí habría un DTO de solicitud normal, validación y una capa de servicio. Pero el principio se ve claramente: si Jackson no puede construir el tipo destino debido a una clave desconocida en la política estricta, el método del controlador no se ejecuta.

Desde el punto de vista de la disciplina de la API, esto es genial: el error se detecta lo más pronto posible. Desde el punto de vista de la experiencia de usuario del cliente, a veces es doloroso: envió un «campo adicional» y el servidor se ofendió.

3. Entrada estricta y tolerante

Los campos desconocidos no tienen un único destino correcto. Se puede ser estricto y rechazar la solicitud. Se puede ser tolerante e ignorar lo adicional. Y ambos enfoques pueden ser correctos en diferentes situaciones. Aquí, como adultos, elegimos no «lo que está más de moda», sino «qué problema estamos resolviendo».

Para no convertir la conversación en filosofía, comparemos los enfoques de la manera más práctica posible:

Estrategia Cómo se comporta el servidor Ventajas Inconvenientes
Entrada estricta «Hay un campo adicional, la solicitud no cumple el contrato, respondo con un error» Detecta rápidamente errores tipográficos del cliente y expectativas incorrectas; el contrato se vuelve «autodocumentado» a través de errores Puede romper clientes ante cambios menores; a veces dificulta integraciones donde el payload es «ruidoso»
Entrada tolerante «Los campos adicionales no molestan: los ignoro, solo leo los conocidos» Útil para migraciones suaves; resistente a clientes «ruidosos»; menos fallos debido a campos no esenciales Puede ocultar errores del cliente (errores tipográficos, colocar el campo en el lugar incorrecto), especialmente si el campo es opcional

El dilema más típico se ve así. El cliente cometió un error tipográfico:

{ "assigneName": "Alice", "title": "..." }

Si la entrada es tolerante, assigneName simplemente se ignorará. Si assigneeName es un campo opcional, la solicitud pasará, pero la tarea quedará sin un responsable asignado, y luego estarás buscando «por qué nuestras tareas están vacías». Esto no es un error, es tu estrategia de «tolerancia».

Si la entrada es estricta, el servidor responderá inmediatamente con un error: «Campo desconocido assigneName». El cliente corregirá el error tipográfico y todos estarán más tranquilos.

Por otro lado, el cliente podría haber enviado campos adicionales no por mala intención, sino porque simplemente tomó el objeto de la respuesta (donde hay id, status, createdAt) y lo envió de vuelta. Técnicamente no está dentro del contrato, pero humanamente es comprensible. En modo tolerante esto funcionará «más o menos», aunque sea discutible.

4. Entrada tolerante: @JsonIgnoreProperties

Ahora pasemos a la herramienta práctica principal de la lección. Si quieres decirle a Jackson: «Cuando leas JSON en este DTO, ignora los campos adicionales», la forma más simple y legible es la anotación @JsonIgnoreProperties(ignoreUnknown=true) en el DTO de solicitud.

El nombre de la anotación es familiar, pero la tarea es diferente: no estamos enumerando propiedades conocidas del DTO que deban ocultarse, sino que establecemos una política para las claves que no existen en el DTO en absoluto.

En nuestro proyecto Task Tracker API esto normalmente se ubicará en el paquete api.dto.request.

Tomemos un fragmento local del DTO de solicitud solo para esta política: los campos aquí son mínimos para que se vea precisamente el comportamiento de ignoreUnknown.

package com.example.tasktracker.api.dto.request;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true) // Le decimos a Jackson: las claves desconocidas en el JSON no se consideran un error
public record TaskCreateRequestTolerantExample(
        String title, // Los campos del DTO son el contrato de entrada: solo leemos lo que está listado aquí
        String description,
        String assigneeName
) {
}

Ahora si el cliente envía:

{
  "title": "Fix pagination bug",
  "description": "Reproducible on page=2",
  "createdAt": "2026-03-21T10:15:00Z",
  "debugMode": true
}

entonces createdAt y debugMode simplemente se ignorarán, y el DTO se seguirá construyendo correctamente. Tu método del controlador recibirá el objeto TaskCreateRequestTolerantExample(title=..., description=..., assigneeName=...) como si nada.

Y aquí es importante mencionar una idea que a menudo se pasa por alto: ignoreUnknown=true no es «menos seguridad». Se trata de hasta qué punto quieres ser un “maestro” para el cliente. Strict — un maestro estricto: «así no se puede». Tolerant — un maestro bondadoso: «vale, he entendido lo que querías decir… supongo».

5. @JsonAlias y entrada tolerante

Cuando hablamos de @JsonAlias, dijimos: un alias es un puente para leer la entrada, pero el contrato debe tener un nombre canónico. En la práctica, el alias a menudo va de la mano con la entrada tolerante, especialmente cuando los payloads de los clientes son «diferentes y maravillosamente diversos».

Mostremos un DTO de solicitud limpio pero realista que:

1) acepta oficialmente assigneeName,
2) también lee assignee como alias,
3) y además no falla ante campos desconocidos.

Ahora tomemos otro fragmento local: aquí necesitamos combinar dos cosas — un alias para un campo conocido y tolerancia hacia claves completamente adicionales.

package com.example.tasktracker.api.dto.request;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true) // Somos tolerantes con las claves adicionales en el JSON
public record TaskCreateRequestCompatibilityExample(
        @JsonProperty("title") // Fijamos explícitamente el nombre del campo en el JSON (nombre canónico del contrato)
        String title,

        String description,

        @JsonProperty("assigneeName") // Nombre canónico del campo que "anunciamos" en la documentación
        @JsonAlias("assignee") // Nombre alternativo para leer la entrada (por ejemplo, para migración/clientes legacy)
        String assigneeName
) {
}

Ahora son posibles varias variantes de entrada.

Canónica (lo que queremos ver en la documentación y en los ejemplos):

{ "title": "Write docs", "assigneeName": "Bob" }

Antigua/alternativa (la entrada se lee, pero no se convierte en «oficial»):

{ "title": "Write docs", "assignee": "Bob" }

Ruidoso (la entrada se lee, lo adicional se ignora):

{
  "title": "Write docs",
  "assignee": "Bob",
  "id": "I-wish-I-could-set-it",
  "createdAt": "yesterday",
  "someWeirdFlag": true
}

Es importante que @JsonAlias aquí resuelve una tarea (nombre alternativo para un campo conocido), mientras que ignoreUnknown=true resuelve otra (ignorar campos desconocidos). Estas mecánicas no compiten, se complementan mutuamente.

6. Elección de la estrategia para Task Tracker API

Ahora apliquemos esto a nuestro proyecto de aprendizaje. Nuestra API es contractual, basada en JSON, y específicamente no queremos convertirla en un «aceptador de cualquier documento JSON de entrada». Por lo tanto, la idea básica es simple: si no hay una razón de peso para tolerar lo adicional, es mejor ser estrictos. Esto te ahorrará horas de depuración y hará que el comportamiento del servidor sea predecible.

Sin embargo, hay casos en los que la entrada tolerante realmente mejora la vida. Por ejemplo, si tienes una integración que envía de forma estable «campos adicionales», pero también envía los necesarios. En tal caso, la ignorancia puede ser un compromiso pragmático: no rompes la integración debido al ruido, mientras sigues controlando el contrato a través del DTO (lo adicional no se mapea al modelo).

El punto clave es precisamente que el DTO te protege del over-posting. Incluso si el cliente envía el campo status o createdAt, no llegará a nuestro objeto de dominio porque en TaskCreateRequest no existe tal campo. En modo tolerante simplemente se ignorará. En modo estricto, la solicitud será rechazada. No es una cuestión de «seguro/inseguro» a nivel de datos. Es una cuestión de «qué comportamiento del contrato queremos».

Y otra regla que sorprendentemente a menudo salva: si un campo en el DTO de solicitud es opcional, la entrada tolerante puede convertir un simple error tipográfico del cliente en un error silencioso. El modo estricto en estos lugares ayuda, porque detectas el error donde se originó, no una semana después en un informe de «por qué nuestro assigneeName es siempre null».

7. Demostración: estricto vs tolerante por HTTP

Para fijar la sensación, es útil ver esto con los ojos del cliente. Imaginemos un endpoint de creación de tarea (ya lo tenemos en el curso) y dos variantes de DTO: estricto y tolerante.

Para comparar el comportamiento en estado puro, tomemos de nuevo dos pequeños fragmentos del mismo punto de entrada de creación. No son dos versiones paralelas del DTO del proyecto, sino el mismo punto de entrada bajo diferentes políticas de lectura de JSON.

Opción A: DTO estricto

package com.example.tasktracker.api.dto.request;

// Variante estricta: cualquier campo desconocido en el JSON puede convertirse en un error de deserialización (dependiendo de la configuración de Jackson)
public record StrictTaskCreateRequest(
        String title, // Contrato de entrada: solo esperamos estos campos
        String description
) {
}

Opción B: DTO tolerante

package com.example.tasktracker.api.dto.request;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true) // Variante tolerante: los campos adicionales en el JSON se ignoran
public record TolerantTaskCreateRequest(
        String title, // Solo leemos los campos conocidos; lo "adicional" no entra en el DTO
        String description
) {
}

Y aquí está la solicitud .http que el cliente podría enviar:

POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
Accept: application/json

{
  "title": "Fix NPE",
  "description": "Null happens in TaskMapper",
  "createdAt": "2026-03-21T10:15:00Z"
}

En modo estricto (si Jackson está configurado para que los campos desconocidos se consideren un error) el servidor responderá con un error, normalmente será un 400 Bad Request. En modo tolerante, la solicitud pasará sin problemas y createdAt desaparecerá como si nunca hubiera existido. Algo así como un calcetín después del lavado: solo tú recuerdas que existió, pero la realidad hace como que no.

Y una vez más, es importante: la entrada tolerante no significa que el servidor «haya aceptado» createdAt. El servidor lo ha ignorado. Si quieres que el cliente entienda con certeza que ha enviado algo adicional, la entrada tolerante no es tu herramienta.

8. Errores típicos con entrada estricta/tolerante

Los errores en este tema generalmente no son sobre la sintaxis de las anotaciones, sino sobre que el desarrollador no es completamente consciente de las consecuencias de la estrategia elegida. Por eso enumero los rastrillos más comunes tal como se ven en un proyecto real: «ayer funcionaba, hoy es extraño».

Error nº1: activar la entrada tolerante «por si acaso» y luego buscar por qué no se detectan los errores del cliente.
Cuando ignoreUnknown=true está en el DTO de solicitud sin motivo, el servidor empieza a «tolerar» incluso errores tipográficos banales. El cliente envió assigneName en lugar de assigneeName — el servidor calló, el campo se volvió null, y el error apareció más tarde y en un lugar donde no es conveniente solucionarlo. Si quieres disciplina en el contrato, la tolerancia debe ser una excepción rara y consciente.

Error nº2: pensar que la entrada tolerante resuelve completamente el problema de la migración del contrato.
La tolerancia a campos desconocidos realmente ayuda cuando el cliente envía información adicional. Pero no ayuda si el cliente envía un campo antiguo que se ha convertido en «conocido pero renombrado». Aquí se necesita @JsonAlias u otra solución explícita. De lo contrario, simplemente ignoras datos que el cliente intentaba transmitir con sentido, y eso es incluso peor que fallar con un error.

Error nº3: usar la entrada tolerante como «reparación oculta» de un DTO deficiente.
A veces el DTO de solicitud está diseñado de tal manera que los clientes encuentran incómodo trabajar con él y constantemente envían campos adicionales (porque tienen un objeto común para todas las operaciones). Activar ignoreUnknown es un parche rápido, pero si se convierte en la norma, es una señal de que vale la pena reconsiderar el contrato. La anotación debe reforzar el diseño, no enmascarar que es incómodo.

Error nº4: activar la tolerancia en el DTO de respuesta y esperar que eso cambie algo.
Los campos desconocidos son una historia sobre la lectura del JSON de entrada. En la salida, serializas el DTO a JSON, y allí simplemente no aparecerán los campos que no existen. Por lo tanto, @JsonIgnoreProperties en un DTO de respuesta generalmente es inútil o induce a error: crea la sensación de que estamos «gestionando la respuesta», cuando en realidad gestionamos el comportamiento de la deserialización.

Error nº5: confundir campos desconocidos con JSON roto e intentar «curar» todo con una sola configuración.
ignoreUnknown no salvará de una coma faltante ni convertirá un JSON roto en uno funcional. Resuelve un problema concreto: «en el JSON hay claves adicionales que no existen en el DTO». Si a menudo recibes JSON malformado, la causa suele estar en el cliente o en un Content-Type incorrecto, no en las anotaciones de Jackson.

1
Tarea
Spring REST & MVC, nivel 12, lección 3
Bloqueada
Deserialización estricta de JSON con campo extra
Deserialización estricta de JSON con campo extra
1
Tarea
Spring REST & MVC, nivel 12, lección 3
Bloqueada
Deserialización tolerante de JSON con clave desconocida
Deserialización tolerante de JSON con clave desconocida
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION