Singular

@Singular

La anotación @Singular se usa junto con @Builder o @SuperBuilder para construir colecciones (listas, conjuntos, mapas) de forma cómoda y segura.

  • Funciona solo dentro de @Builder o @SuperBuilder.

✅ ¿Para qué sirve?

Permite agregar elementos uno a uno al builder, en lugar de pasar una lista completa.

import lombok.experimental.SuperBuilder;
import lombok.Singular;

import java.util.List;

@SuperBuilder
class Persona {
    String nombre;

    @Singular
    List<String> hobbies;
}
Persona persona = Persona.builder()
    .nombre("Laura")
    .hobby("Leer")
    .hobby("Viajar")
    .build();

Sin @Singular tendrias que hacer esto

Persona persona = Persona.builder()
    .nombre("Laura")
    .hobbies(Arrays.asList("Leer", "Viajar"))
    .build();

¿Qué genera Lombok con @Singular?

Al poner @Singular List<String> hobbies, Lombok genera automáticamente:

  • hobby(String hobby) → Agrega un solo hobby.
  • hobbies(Collection<String> hobbies) → Agrega varios.
  • clearHobbies() → Limpia la lista.

Consideraciones

  • Lombok intenta convertir el nombre de la colección al singular automáticamente (ej. hobbieshobby). Esto funciona bien con palabras en inglés comunes.

    Si tu colección no es fácil de convertir, puedes indicar manualmente el singular:

@Singular("pasatiempo")
List<String> pasatiempos;
.builder()
  .pasatiempo("Leer")
  .pasatiempo("Cantar");
  • Soporta las siguientes collecciones
    • java.util:
      • IterableCollection, and List (backed by a compacted unmodifiable ArrayList in the general case).
      • SetSortedSet, and NavigableSet (backed by a smartly sized unmodifiable HashSet or TreeSet in the general case).
      • MapSortedMap, and NavigableMap (backed by a smartly sized unmodifiable HashMap or TreeMap in the general case).
    • Guava’s com.google.common.collect:
      • ImmutableCollection and ImmutableList (backed by the builder feature of ImmutableList).
      • ImmutableSet and ImmutableSortedSet (backed by the builder feature of those types).
      • ImmutableMapImmutableBiMap, and ImmutableSortedMap (backed by the builder feature of those types).
      • ImmutableTable (backed by the builder feature of ImmutableTable).

Personalización

ignoreNullCollections = true

@Singular(ignoreNullCollections = true)
List<String> hobbies;

Comportamiento por defecto (sin ignoreNullCollections)

Persona.builder()
  .hobbies(null)  // ❌ Esto lanza NullPointerException
  .build();

con ignoreNullCollections = true

En su lugar: Ignora el null , no agrega nada, sigue funcionando normalmente

Persona.builder()
  .hobbies(null)  // ✅ No hace nada, no lanza excepción
  .build();

Singular with Jackson

  • Lombok genera automáticamente un builder para tu clase cuando usas @Builder o @SuperBuilder.
  • Jackson, que se usa para serializar/deserializar JSON, necesita a veces configuraciones especiales para que el builder funcione bien con sus reglas.
  • Para que Jackson use el builder generado por Lombok durante la deserialización, debes indicarle explícitamente que use el builder con la anotación @JsonDeserialize(builder=...).
@Value @Builder
@JsonDeserialize(builder = JacksonExample.JacksonExampleBuilder.class)
public class JacksonExample {
	@Singular(nullBehavior = NullCollectionBehavior.IGNORE) private List<Foo> foos;
	
	@JsonPOJOBuilder(withPrefix = "")
	public static class JacksonExampleBuilder implements JacksonExampleBuilderMeta {
	}
	
	private interface JacksonExampleBuilderMeta {
		@JsonDeserialize(contentAs = FooImpl.class) 
        JacksonExampleBuilder foos(List<? extends Foo> foos);
	}
}

1. @JsonDeserialize(builder = JacksonExample.JacksonExampleBuilder.class)

Le dices a Jackson:

Para deserializar esta clase desde JSON, usa este builder que está dentro de la clase.

2. Definir el builder manualmente

  • Aunque Lombok genera el builder automáticamente, aquí defines explícitamente la clase JacksonExampleBuilder.
  • Esto te permite añadir anotaciones propias de Jackson o añadir métodos personalizados.

3. @JsonPOJOBuilder(withPrefix = "")

  • Indica que los métodos del builder no tienen prefijo (por defecto Jackson busca métodos con prefijo with o set).
  • Por ejemplo, el método foos(...) en el builder será reconocido correctamente sin necesidad de un prefijo como withFoos.

4. Interfaz JacksonExampleBuilderMeta

  • Aquí defines métodos que quieres personalizar, como el método foos(List<? extends Foo> foos) para el builder.
  • Anotas este método con @JsonDeserialize(contentAs = FooImpl.class) para indicarle a Jackson que los elementos de la lista foos deben deserializarse como FooImpl, que es una implementación concreta de la interfaz Foo.

Documentación oficial