DynamoDB PutItem en Java (AWS SDK v2)

PutItem escribe un elemento entero y reemplaza cualquier elemento existente con la misma clave principal (acciones sobre elementos cubre en qué se diferencia de UpdateItem). En AWS SDK for Java 2.x cada atributo entra en un PutItemRequest como un AttributeValue tipado, y el builder te dejará construir uno que no puede ser válido de ninguna manera.

Código

import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

public class PutItemExample {
    public static void main(String[] args) {
        try (DynamoDbClient ddb = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build()) {

            Map<String, AttributeValue> item = new HashMap<>();
            item.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
            item.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());
            item.put("AlbumTitle", AttributeValue.builder().s("Danzon").build());
            item.put("Year", AttributeValue.builder().n("1994").build());
            item.put("Awards", AttributeValue.builder().n("0").build());

            Map<String, String> names = new HashMap<>();
            names.put("#cond0", "Artist");
            names.put("#cond1", "SongTitle");

            PutItemRequest request = PutItemRequest.builder()
                    .tableName("Music")
                    .item(item)
                    .conditionExpression("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)")
                    .expressionAttributeNames(names)
                    .build();

            try {
                ddb.putItem(request);
                System.out.println("Song written");
            } catch (ConditionalCheckFailedException e) {
                System.out.println("A song with that key already exists — not overwritten");
            }
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

Explicación

AttributeValue.builder().build() compila. Y además no se puede enviar. El builder no tiene ningún campo obligatorio, así que un atributo en el que olvidaste el .s(...) pasa la comprobación de tipos perfectamente y falla en el servicio:

DynamoDbException | ValidationException | Supplied AttributeValue is empty, must contain exactly one of the supported datatypes

Esta es la forma específica de Java de un error que otros SDK hacen imposible: los types.AttributeValueMember* de Go son tipos separados, así que no hay nada que dejar sin asignar. Consulta "Supplied AttributeValue is empty" para la solución general.

Un null de Java pasado a .s(...) no se convierte en un NULL de DynamoDB. Esta es la versión que muerde de verdad, porque parece un valor:

AttributeValue.builder().s(customer.getNotes()).build()   // getNotes() returned null

No se lanza ninguna NullPointerException en la construcción. El builder simplemente no registra nada, y la petición falla con el mismo mensaje Supplied AttributeValue is empty, apuntando a un atributo del que nunca sospechaste. Si quieres un null de verdad, eso es AttributeValue.builder().nul(true).build(); casi siempre lo que quieres es omitir la entrada. Fíjate en que esto es lo contrario del SDK de Go, donde un puntero nil se serializa como NULL y crea el atributo en silencio; ambos se ejecutaron contra el mismo motor el mismo día.

getMessage() no es el mensaje del servicio. El SDK añade su propio contexto, así que la cadena es:

The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: 77a08ef1-a3a9-4f97-b309-4cb0741edd1a) (SDK Attempt Count: 1)

Compara con e.awsErrorDetails().errorCode() y lee e.awsErrorDetails().errorMessage() para el texto desnudo. Cualquier cosa que compare getMessage() con un literal se rompe con un reintento, que cambia el contador de intentos.

Captura ConditionalCheckFailedException antes que DynamoDbException, y lee lo que trae. Extiende DynamoDbException, así que ordenar los bloques catch al revés hace inalcanzable el handler específico. Sobre el objeto capturado: statusCode() devolvió 400 y retryable() devolvió false, que es la respuesta honesta para un rechazo de lógica de negocio. Añade .returnValuesOnConditionCheckFailure("ALL_OLD") a la petición y e.item() vuelve relleno con el elemento que bloqueó la escritura (cinco atributos en la ejecución de arriba, Year como AttributeValue(N=1994)), así que no necesitas un getItem de seguimiento para averiguar quién ganó.

Los números entran como cadenas mediante .n(...). El tipo N de DynamoDB es texto decimal en la red, y eso es lo que evita que 1994 se convierta en un double. .n(String.valueOf(year)) es el idioma; no hay ninguna sobrecarga .n(int) a la que recurrir.

El cliente es Closeable, y de vida larga. El try-with-resources de arriba está bien para un programa de un solo uso y mal para un servicio: DynamoDbClient es dueño de un pool de conexiones HTTP y es seguro para hilos, así que construye uno por aplicación y déjalo vivir. Construir uno por petición es el bug de rendimiento más común de Java sobre esta API.

¿Prefieres beans a mapas de AttributeValue? El DynamoDB Enhanced Client (software.amazon.awssdk.enhanced.dynamodb) mapea una clase anotada directamente a un elemento, lo que elimina por completo la trampa del builder vacío. Cuesta un escaneo reflexivo de TableSchema.fromBean al arrancar, que StaticTableSchema evita si eso te importa.

Hazlo visualmente

Todos los fallos de arriba empiezan con valores tipados construidos a mano. El conversor de JSON de DynamoDB gratuito toma JSON normal y devuelve la forma tipada, así que puedes ver exactamente cómo debería quedar el elemento en la red antes de escribir un solo AttributeValue.builder().

Para escribir y editar elementos contra tus propias tablas — un formulario por atributo, selectores de tipo, copiar el resultado de vuelta como Java — descarga DynoTable.

Ejemplos relacionados

Referencias

Reproducido el 2026-07-28 con AWS SDK for Java 2.49.4 sobre OpenJDK 26.0.1, contra DynamoDB Local (amazon/dynamodb-local) en el puerto 9000. El texto de las excepciones, el código de estado y el contenido del elemento de arriba son salida capturada, copiada literalmente.

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.