CodeGym /Cursos /Spring REST & MVC /JsonNode y TypeReference en PATCH

JsonNode y TypeReference en PATCH

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

1. Cuando un DTO no es suficiente en PATCH

Si miramos PATCH desde el punto de vista del cliente, todo parece simple: «solo envío lo que quiero cambiar». Pero si lo miramos desde el punto de vista del servidor, surge un momento complicado: en Java null es un único valor, mientras que en JSON tenemos dos estados diferentes: «el campo está ausente» y «el campo está presente pero es null». Para PATCH estas son intenciones fundamentalmente diferentes, especialmente para campos opcionales como description o assigneeName. Y aquí un TaskPatchRequest común como record con un String description empieza repentinamente a difuminar el significado: tanto la ausencia del campo como {"description":null} se convierten en description() == null. El servidor tiene que adivinar, y adivinar no es un contrato, es una lotería (donde el gran premio es un incidente nocturno en producción).

Imaginemos que queremos admitir el siguiente comportamiento para los campos de una tarea:

  • si el cliente envió "description": "..." — cambiamos la descripción;
  • si el cliente envió "description": null — limpiamos la descripción (establecemos null);
  • si el cliente no envió description — no tocamos el valor actual.

Con un DTO común, la lógica ingenua de la lección anterior se ve así:

public void applyPatch(Task current, TaskPatchRequest patch) {
    // Si el valor no es null — actualizamos el campo
    // IMPORTANTE: con este enfoque, "limpiar el campo" es imposible, porque la limpieza también es null.
    if (patch.description() != null) {
        current.setDescription(patch.description());
    }
}

Y aquí está el problema: ¿cómo «limpiar description» si la limpieza también se expresa como null? De ninguna manera. El DTO no sabe si el campo estaba en el JSON. Por lo tanto, necesitamos una fuente adicional de información, no sobre el valor, sino sobre el hecho de que el campo está presente. Esto es exactamente lo que proporciona JsonNode: el árbol de la solicitud en el que se puede preguntar «¿estaba el campo?» independientemente de «¿cuál es su valor?».

Para que no tengamos la sensación de que estamos «traicionando al DTO», fijemos una regla muy práctica del curso: el DTO sigue siendo la forma principal de diseñar el contrato, y JsonNode es una lupa/microscopio local que activamos cuando necesitamos ver detalles adicionales del JSON de entrada (estructura, presencia del campo, null explícito).

2. JsonNode como árbol JSON

Cuando usas @RequestBody TaskPatchRequest, Spring MVC a través de Jackson deserializa el JSON en un objeto Java. Esto es cómodo, seguro en cuanto a tipos y legible. Pero tiene un precio: el JSON se convierte en «valores de campos del DTO» y parte de la información contextual se pierde. JsonNode es una visión diferente de los mismos datos: obtienes no un «objeto listo», sino un árbol de nodos donde se puede preguntar por separado qué claves llegaron, qué valores tienen y si son null. Es como si, en lugar de un «plato preparado», te dieran la receta y los ingredientes: cocinar es un poco más complicado, pero sabes exactamente si había carne o era solo aroma de ahumado.

Es importante no confundir JsonNode con Map<String, Object>. Sí, ambos enfoques son «dinámicos», pero en cuanto a sensaciones y posibilidades son diferentes. JsonNode conserva claramente la semántica JSON (object/array/value/null/missing), permite verificar el tipo de nodo y maneja con bastante cuidado los campos ausentes. Map<String, Object> se parece más a una «bolsa de cosas»: allí es fácil perder tipos, obtener un LinkedHashMap inesperado en lugar de un DTO anidado y comenzar accidentalmente una fiesta de instanceof que termina en lágrimas.

Una pequeña tabla comparativa (como chuleta, no como religión):

Enfoque Qué obtenemos Fortalezas Debilidades Dónde es apropiado en el curso
DTO Objeto tipado Claro, seguro, cómodo para el contrato Se pierde el hecho de la presencia del campo; null es ambiguo Camino principal para la API
JsonNode Árbol JSON Vemos absent vs null; vemos la estructura; podemos verificar el tipo Requiere más código; es fácil abusar de la dinámica Localmente: semántica PATCH, fragmentos dinámicos pequeños
Map<String, Object> Contenedor dinámico Rápido, flexible, cómodo para un bloque «extra» pequeño Los tipos son imprecisos; es fácil caer en instanceof Solo para bloques dinámicos pequeños y explícitamente limitados

Dicho de forma breve y humana: DTO es un contrato normal, JsonNode es una lupa y Map<String, Object> es una «bolsa de la compra sin recibo» (parece que está todo, pero qué exactamente, lo descubres ya en casa).

3. Métodos importantes de JsonNode

Casi todos los problemas de los principiantes con JsonNode surgen porque esperan del árbol JSON un «comportamiento de objeto normal», pero es diferente. JsonNode tiene varios métodos que suenan similares pero se comportan de manera diferente. Y si eliges el equivocado, puedes obtener un NullPointerException incluso donde parecía «¡pero si lo he comprobado!». Es como con String y StringBuilder: ambos «son sobre cadenas», pero si no se usan correctamente, será triste y a veces incluso lento.

Empecemos con un pequeño ejemplo: las diferencias entre get(...) y path(...) — es un clásico que vale la pena aprender de una vez para no tener que depurar después «por qué falla a veces».

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();

// Leemos JSON como árbol
JsonNode root = mapper.readTree("{\"title\":\"Refine API\"}");

// get(...) ante la ausencia de clave devuelve exactamente null
JsonNode v1 = root.get("missing");     // devuelve null

// path(...) ante la ausencia de clave devuelve MissingNode (un "placeholder" seguro)
JsonNode v2 = root.path("missing");    // devuelve MissingNode (no null)

// Demostración de las diferencias
System.out.println(v1 == null);         // true
System.out.println(v2.isMissingNode()); // true

Observa un detalle importante: root.get("missing") devuelve exactamente null, por lo tanto cualquier llamada a root.get("missing").isNull() fallará. En cambio, path(...) devuelve un nodo especial MissingNode, con el que se puede trabajar de forma segura.

Otro par útil es has(...) y hasNonNull(...). El primero responde a la pregunta «¿existe la clave?», el segundo — «¿existe la clave y el valor no es null?». Para PATCH a menudo necesitamos precisamente has(...), porque queremos distinguir {"field":null} de {}.

import com.fasterxml.jackson.databind.JsonNode;

// root — es un JsonNode, por ejemplo obtenido con mapper.readTree(...)
boolean hasDescription = root.has("description");               // ¿está presente el campo?
boolean hasNonNullDescription = root.hasNonNull("description"); // ¿presente y no es null?

En la semántica PATCH, «el campo está presente pero es null» suele ser un comando independiente (por ejemplo, limpiar el valor). Por lo tanto, hasNonNull(...) puede ser incluso perjudicial: nos «oculta» el null explícito.

Una pequeña chuleta de los métodos más necesarios en el contexto de PATCH:

Método Qué pregunta Comportamiento ante ausencia ¿Útil para PATCH?
has("x") «¿la clave x estaba?» false Sí, es el principal
get("x") «dame el nodo x» null Con cuidado
path("x") «dame el nodo x, pero seguro» MissingNode Sí, cómodo
isNull() «¿el valor es explícitamente JSON null?» — Sí
asText() «leer como cadena» en MissingNode devuelve "" Con cuidado, mejor verificar primero

El último punto es especialmente importante: asText(...) en un MissingNode puede devolver una cadena vacía, y si no has comprobado has(...), convertirás repentinamente «el campo está ausente» en «cadena vacía». Este es uno de los errores lógicos más insidiosos: la aplicación funciona, pero no hace lo que debe.

4. JsonNode en la API de Task Tracker para PATCH

La forma más saludable de incorporar JsonNode en nuestro proyecto es usarlo de forma puntual en el límite de lectura del cuerpo HTTP, exactamente donde necesitamos distinguir entre absent y explicit null. El contrato no se vuelve dinámico: a continuación, todavía queremos trabajar con un TaskPatchRequest tipado para no tener que analizar manualmente fechas, enum’s y listas. La idea es simple: el árbol (JsonNode) responde a la pregunta «¿estaba el campo?», y el DTO (TaskPatchRequest) responde a la pregunta «¿cuál es el valor y de qué tipo?». Es como el trabajo de dos empleados: uno verifica si el paquete llegó, y el segundo lo desempaqueta y mira qué hay dentro.

Empecemos con el controlador. Para la demostración, aceptaremos JsonNode como cuerpo de la solicitud. Externamente, el contrato sigue siendo un objeto JSON con campos conocidos, solo que en Java no lo vinculamos directamente a un DTO por ahora.

import com.example.tasktracker.api.dto.response.TaskDetailsResponse;
import com.example.tasktracker.api.mapper.TaskMapper;
import com.example.tasktracker.domain.model.Task;
import com.example.tasktracker.domain.service.TaskService;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;
    private final TaskMapper taskMapper;

    public TaskController(TaskService taskService, TaskMapper taskMapper) {
        this.taskService = taskService;
        this.taskMapper = taskMapper;
    }

    @PatchMapping("/{taskId}")
    public TaskDetailsResponse patchTask(
            @PathVariable String taskId,
            @RequestBody JsonNode patchJson // aceptamos el árbol para ver "campo estaba/no estaba"
    ) {
        // El controlador sigue siendo delgado: delegamos la lógica de negocio al servicio
        Task task = taskService.patch(taskId, patchJson);

        // Mapeo del modelo de dominio al DTO de respuesta
        return taskMapper.toDetailsResponse(task);
    }
}

Observa que el controlador sigue siendo delgado: no hace lógica de merge ni analiza el JSON. Solo recibe la entrada y delega. El «trabajo sucio» está en el servicio.

Si dejamos que JsonNode se adentre en la capa de servicio, el contrato tipado de PATCH se difuminará de nuevo. Por lo tanto, es útil mantener el árbol cerca del límite de la capa web: se encarga de la presencia del campo, y luego es mejor convertir rápidamente los datos a un DTO u otro portador tipado.

Ahora el servicio. Dentro del servicio queremos obtener valores tipados, por lo tanto convertimos el árbol en un TaskPatchRequest. En Jackson, esto se hace cómodamente con treeToValue(...). Importante: tomamos el ObjectMapper de Spring (lo inyectamos), no creamos uno nuevo «como sea», de lo contrario perderás la configuración consistente (especialmente para fechas).

import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TaskServiceImpl {

    private final ObjectMapper objectMapper;

    public TaskServiceImpl(ObjectMapper objectMapper) {
        // Importante inyectar el ObjectMapper de Spring para no perder la configuración general
        this.objectMapper = objectMapper;
    }

    public TaskPatchRequest readPatch(JsonNode patchJson) throws Exception {
        // Convertimos el árbol JSON en un DTO tipado
        // (los enums, fechas, etc. serán procesados por las reglas de Jackson)
        return objectMapper.treeToValue(patchJson, TaskPatchRequest.class);
    }
}

Aquí el código es aún un «semiacabado», pero la lógica es clara: TaskPatchRequest proporciona tipos normales para fechas, enum’s y listas, mientras que JsonNode conserva el hecho de la presencia del campo. Estos dos roles no compiten, sino que se complementan.

Ahora lo más importante: la aplicación del parche al estado actual. Queremos distinguir la situación «el campo está ausente» de «el campo está presente y es null». El truco más simple y legible es: verificar patchJson.has("description"), y si es así, aplicar el valor del DTO (incluyendo null, que ahora se interpreta como «limpiar»).

import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.example.tasktracker.domain.model.Task;
import com.fasterxml.jackson.databind.JsonNode;

public class TaskPatchApplier {

    public void apply(Task current, TaskPatchRequest dto, JsonNode json) {
        // title: aquí importa el hecho mismo de que el campo esté presente.
        // Si el campo llegó pero el valor es null, eso ya no es "no tocar", sino una violación del contrato.
        if (json.has("title")) {
            if (dto.title() == null) {
                throw new IllegalArgumentException("title cannot be null");
            }
            current.setTitle(dto.title());
        }

        // description: distinguimos "el campo está ausente" de "el campo está y es null"
        if (json.has("description")) {
            // dto.description() puede ser null -> esto es un comando separado "limpiar"
            current.setDescription(dto.description());
        }

        // assigneeName: la misma lógica PATCH que para description
        if (json.has("assigneeName")) {
            current.setAssigneeName(dto.assigneeName());
        }
    }
}

Aquí se ve claramente la separación de roles: json.has("title") responde a la pregunta «¿estaba el campo?», y dto.title() responde a «¿qué valor llegó y de qué tipo es?».

Para campos donde null no debe significar un comando separado, JsonNode sigue siendo necesario: permite distinguir «el campo no se tocó en absoluto» de «el campo se envió como null y se violó el contrato».

// priority: aquí la misma lógica que con title.
// Nos importa distinguir "el campo no se tocó" de "el campo se envió como null y se violó el contrato".
if (json.has("priority")) {
    if (dto.priority() == null) {
        throw new IllegalArgumentException("priority cannot be null");
    }
    current.setPriority(dto.priority());
}

En este punto es importante no mezclar dos tareas diferentes. JsonNode lo necesitamos para la semántica PATCH: nos ayuda a ver si el campo estaba en el JSON y si llegó un null explícito. TypeReference resuelve otra cuestión: cómo conservar el tipo genérico cuando leemos contenedores JSON directamente a través de ObjectMapper.

5. TypeReference y el borrado de tipos

En algún momento empezarás a leer o escribir JSON no solo a través de Spring MVC (donde todo lo hace @RequestBody), sino también directamente a través de ObjectMapper: en pequeñas utilidades, en experimentos, en algunos análisis locales de JSON, y a veces solo para entender qué está sucediendo. Y allí te encontrarás con TypeReference. Parece una clase anónima extraña, y la primera reacción de un principiante suele ser: «¿por qué este chamán, no se puede simplemente escribir List<TaskPatchRequest>.class?» Spoiler: no se puede, por culpa de Java.

El problema se llama «borrado de tipos» (type erasure). En compilación, List<TaskPatchRequest> y List<CommentCreateRequest> para la JVM se convierten simplemente en List. La información sobre el parámetro de tipo (TaskPatchRequest) no se almacena en tiempo de ejecución como nos gustaría. Jackson, cuando lee JSON, también vive en tiempo de ejecución. Si le pides que lea «simplemente List», leerá honestamente una lista, pero los elementos los hará lo más genéricos posible (normalmente LinkedHashMap para objetos). Y esto no es un error de Jackson, es una respuesta honesta a una pregunta honesta: «pediste una lista de algo — te la di».

TypeReference es una forma de indicarle a Jackson un tipo más preciso con parámetros genéricos. Básicamente, creas un objeto que «lleva» la información: «esto no es solo un List, es List<TaskPatchRequest>». Parece magia, pero en realidad es solo un workaround alrededor de las limitaciones de los genéricos en Java.

6. Práctica de TypeReference

Empecemos con un ejemplo simple pero muy ilustrativo: un array JSON de solicitudes de parche. Sí, en una API real no parchearemos así (las operaciones batch no las hacemos hoy), pero como ejemplo académico muestra perfectamente el problema.

Primero, intentemos «incorrectamente» — leer JSON como List.class. Jackson no sabe qué elementos hay dentro, por lo tanto los leerá como Map.

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;

ObjectMapper mapper = new ObjectMapper();

String json = "[{\"title\":\"A\"},{\"title\":\"B\"}]";

// Pedimos a Jackson que lea "simplemente una lista" sin información sobre el tipo de los elementos
List
   raw = mapper.readValue(json, List.class);

// Por lo tanto, los elementos serán lo más genéricos posible (normalmente Map)
System.out.println(raw.get(0).getClass()); // class java.util.LinkedHashMap

En apariencia funciona, pero ahora en lugar de TaskPatchRequest tienes un LinkedHashMap. Se puede, por supuesto, mapear manualmente cada elemento, pero es como lavar los platos con un cepillo de dientes: técnicamente posible, pero ¿para qué?

Ahora hagámoslo correctamente: usamos TypeReference<List<TaskPatchRequest>>. Observa que el JSON es el mismo.

import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;

ObjectMapper mapper = new ObjectMapper();

String json = """
        [
          { "title": "A" },
          { "title": "B" }
        ]
        """;

// Pasamos un TypeReference para conservar el tipo genérico de los elementos de la lista
List<TaskPatchRequest> items = mapper.readValue(json, new TypeReference<>() {});

// Ahora los elementos son exactamente DTO, no Map
System.out.println(items.get(0).title()); // A

Y ahora Jackson crea una lista exactamente de nuestros DTO, con campos y tipos normales. Esto es especialmente importante cuando los elementos contienen enum’s, fechas y estructuras anidadas: sin TypeReference obtendrías mapas, no un modelo de objetos.

Ahora el segundo escenario: un bloque «extra» dinámico pequeño. Aquí no necesitamos un DTO para cada campo, porque el sentido del bloque está en la flexibilidad, pero queremos leerlo de forma ordenada y al menos tipada a nivel de contenedor.

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

ObjectMapper mapper = new ObjectMapper();

String json = "{\"source\":\"import\",\"attempt\":2}";

// Aquí reconocemos la "dinamicidad": leemos como Map, pero fijamos el tipo del contenedor
Map<String, Object> extra = mapper.readValue(json, new TypeReference<>() {});

System.out.println(extra.get("source")); // import

Sí, dentro de Object seguirá siendo Object, y los números pueden convertirse en Integer, Long o Double — es normal para un bloque dinámico. Lo importante es que reconoces honestamente: «esta parte del documento es dinámica», y no intentas convertirla en un «DTO universal para todo».

7. JSON mixto: DTO + dinámica

La parte más agradable de este tema es que Jackson soporta perfectamente el «mix-and-match»: puedes tomar un solo documento JSON, leer la parte tipada en un DTO y un pequeño fragmento dinámico por separado. Esto permite mantener el contrato estricto donde debe ser estricto, y al mismo tiempo no romperse la cabeza donde la «tipificación perfecta» simplemente no vale el esfuerzo. Lo principal es mantener el límite: la dinámica debe ser pequeña y estar claramente marcada, de lo contrario el proyecto comienza gradualmente a convertirse en un «servidor que acepta cualquier JSON y finge que todo está bajo control».

Supongamos que tenemos un documento hipotético (solo para demostración de la técnica) donde hay un objeto conocido taskPatch y un extra dinámico adicional:

{
  "taskPatch": { "assigneeName": null, "priority": "HIGH" },
  "extra": { "source": "import", "attempt": 2 }
}

En código podría verse así: primero leemos el árbol, luego la parte conocida la convertimos en TaskPatchRequest, y la dinámica en Map<String, Object>.

import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

ObjectMapper mapper = new ObjectMapper();

String json = """
        {
          "taskPatch": { "assigneeName": null, "priority": "HIGH" },
          "extra": { "source": "import", "attempt": 2 }
        }
        """;

// 1) Leemos el documento original como árbol, para poder "recortar" piezas
JsonNode root = mapper.readTree(json);

// 2) La parte tipada la leemos en un DTO (obtendremos enum/fechas/validación de formato según las reglas de Jackson)
TaskPatchRequest patch = mapper.treeToValue(root.get("taskPatch"), TaskPatchRequest.class);

// 3) La parte dinámica la leemos en un Map: la forma es flexible, pero el límite de la dinámica es explícito
Map<String, Object> extra = mapper.convertValue(root.get("extra"), new TypeReference<>() {});

Observa dos cosas. En primer lugar, la parte tipada (patch) sigue siendo tipada: priority se convertirá en TaskPriority, y si el cliente envía un enum «incorrecto», Jackson lo notará (y cómo manejaremos el error hacia afuera lo veremos en el módulo sobre manejo de errores, pero hoy nos importa la separación de responsabilidades). En segundo lugar, extra sigue siendo flexible y limitado: no dicta la forma de todo el modelo y no nos obliga a renunciar al DTO.

En el contexto de la API de Task Tracker, esta técnica se puede mantener como una «herramienta de ingeniería», no como una parte obligatoria del contrato. Es decir, no estamos obligados a añadir extra al PATCH público ahora mismo, pero ya sabemos cómo hacerlo si en el futuro surge una razón real (por ejemplo, un cliente de migración, importación, integración). Y eso es precisamente «flexibilidad controlada», no caos.

Para el propio Task PATCH, el camino de trabajo aquí sigue siendo uno solo: en el límite de la capa web conservamos el hecho de la presencia del campo a través de JsonNode, los valores los leemos en un DTO tipado y aplicamos las reglas por campo explícitamente. TypeReference simplemente se queda al lado como una herramienta de Jackson para contenedores genéricos y fragmentos dinámicos pequeños.

8. Errores típicos al trabajar con JsonNode y TypeReference

Error n.º 1: usar JsonNode en todas partes «por si acaso», porque es más fácil.
Al principio realmente parece más fácil: no hay que crear DTO, no hay que pensar en tipos, puedes simplemente escribir root.get("x").asText(). Pero después de una semana, de repente te das cuenta de que en tu proyecto no hay contrato, no hay tipos normales, y cualquier error se convierte en un juego de adivinanzas. Mantén JsonNode como una herramienta puntual: para determinar la presencia de un campo, ver un explicit null, leer un bloque dinámico pequeño. Todo lo demás que siga siendo DTO.

Error n.º 2: confundir get(...) y path(...), y luego encontrarse con un NullPointerException.
root.get("missing") devuelve null, y si olvidaste la comprobación, habrá una caída. En ejemplos académicos esto se detecta rápido, pero en código real a veces «cae solo en una petición extraña» y buscas la causa durante mucho tiempo. Si recorres el árbol sin garantía de que el campo exista, el hábito de usar path(...) en lugar de get(...) generalmente hace el código más robusto y la vida más tranquila.

Error n.º 3: verificar solo el valor, pero no verificar la presencia del campo.
Esta es la trampa principal de PATCH: escribes if (dto.description() == null) y piensas que es un «comando para limpiar», pero en realidad podría ser «el campo no se envió en absoluto». Como resultado, o no podrás limpiar el campo, o empezarás a limpiarlo accidentalmente. Para PATCH, primero averigua «¿estaba el campo?», y luego «¿cuál es el valor?».

Error n.º 4: intentar leer List<MyDto> sin TypeReference y luego sorprenderse con un LinkedHashMap.
Jackson no es telépata. Si le dices «lee un List», leerá una lista de «objetos cualesquiera» y devolverá un Map. No es que «Jackson sea malo», es una «solicitud demasiado genérica». En cuanto leas estructuras genéricas directamente a través de ObjectMapper, ten en mente: TypeReference es una necesidad normal, no un capricho extraño de la librería.

Error n.º 5: convertir un Map<String, Object> dinámico en el contrato público principal.
Normalmente esto comienza como un pequeño bloque «extra» «para experimentar», y termina en un endpoint que acepta «lo que sea», porque «a los clientes les es más cómodo». Luego comienza la validación manual, errores manuales, conversiones manuales, y el código empieza a parecerse a un mini-intérprete de lenguaje JSON. Si se necesita dinamismo, limítalo a un pequeño fragmento, mantenlo en un solo lugar, y no dudes en decir «no, este campo está tipado».

1
Tarea
Spring REST & MVC, nivel 13, lección 3
Bloqueada
`JsonNode` para determinar presencia y null explícito
`JsonNode` para determinar presencia y null explícito
1
Tarea
Spring REST & MVC, nivel 13, lección 3
Bloqueada
JSON mixto: typed DTO más bloque dinámico mediante `TypeReference`
JSON mixto: typed DTO más bloque dinámico mediante `TypeReference`
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION