Repository navigation
Open
Labels
bugThis issue is a bug.This issue is a bug.dynamodb-enhancedp2This is a standard priority issueThis is a standard priority issuethird-partyThis issue is related to third-party libraries or applications.This issue is related to third-party libraries or applications.
Description
Describe the bug
It is possible to declare a DynamoDB entity class with such a field:
private String __encryption_keyId;
... and use the entity when it's annotated @DynamoDbBean but it's not possible when it's annotated @DynamoDbImmutable.
Expected Behavior
Consistent behavior for both @DynamoDbBean and @DynamoDbImmutable annotations. As it's already working for @DynamoDbBean, it should work for @DynamoDbImmutable too, rather than copying a restriction from @DynamoDbImmutable to @DynamoDbBean.
Current Behavior
TableSchema.fromClass(...) throws an exception:
java.lang.IllegalArgumentException: A method was found on the immutable class that does not appear to have a matching setter on the builder class. Use the @DynamoDbIgnore annotation on the method if you do not want it to be included in the TableSchema introspection. [Method = "public java.lang.String com.example.UnderscoreFieldTest$Immutable.get__encryption_keyId()"]
at software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector.generateExceptionForMethod(ImmutableIntrospector.java:134)
at software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector.lambda$introspect$1(ImmutableIntrospector.java:87)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector.lambda$introspect$2(ImmutableIntrospector.java:86)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector.introspect(ImmutableIntrospector.java:93)
at software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector.getImmutableInfo(ImmutableIntrospector.java:65)
at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema.createStaticImmutableTableSchema(ImmutableTableSchema.java:174)
at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema.create(ImmutableTableSchema.java:145)
at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema.lambda$create$0(ImmutableTableSchema.java:134)
at java.base/java.util.Map.computeIfAbsent(Map.java:1054)
at java.base/java.util.Collections$SynchronizedMap.computeIfAbsent(Collections.java:2760)
at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema.create(ImmutableTableSchema.java:133)
at software.amazon.awssdk.enhanced.dynamodb.TableSchema.fromImmutableClass(TableSchema.java:141)
at software.amazon.awssdk.enhanced.dynamodb.TableSchema.fromClass(TableSchema.java:164)
Reproduction Steps
Run the test:
package com.example;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Value;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import static org.assertj.core.api.Assertions.assertThatCode;
class UnderscoreFieldTest {
@Test
void shouldResolveTableSchemaFromBean() {
assertThatCode(() -> TableSchema.fromClass(Bean.class)).doesNotThrowAnyException();
}
@Test
void shouldResolveTableSchemaFromImmutable() {
assertThatCode(() -> TableSchema.fromClass(Immutable.class)).doesNotThrowAnyException();
}
@Data
@DynamoDbBean
public static class Bean {
@Getter(onMethod_ = @DynamoDbPartitionKey)
private String id;
private String __encryption_keyId;
}
@Value
@Builder
@DynamoDbImmutable(builder = Immutable.ImmutableBuilder.class)
public static class Immutable {
@Getter(onMethod_ = @DynamoDbPartitionKey)
private String id;
private String __encryption_keyId;
}
}
Dependencies:
software.amazon.awssdk:dynamodb-enhancedversion2.27.19org.springframework.boot:spring-boot-starter-testversion3.3.3org.projectlombok:lombokversion1.18.34
Possible Solution
Release restrictions for @DynamoDbImmutable annotation.
Additional Information/Context
No response
AWS Java SDK version used
2.27.19
JDK version used
openjdk version "17.0.3" 2022-04-19
Operating System and version
multiple (macOS 14.6.1, Ubuntu Linux 22.04 LTS)
Metadata
Metadata
Assignees
Labels
bugThis issue is a bug.This issue is a bug.dynamodb-enhancedp2This is a standard priority issueThis is a standard priority issuethird-partyThis issue is related to third-party libraries or applications.This issue is related to third-party libraries or applications.
Activity
debora-ito commentedon Feb 7, 2026
@klach-ocado sorry for the lack of updates.
I can reproduce. My guess is this related to how the Enhanced Client doesn't work well with lombok (#1932), even though it's working with DynamoDbBean.
I ran a quick test and the double underscore works fine with regular non-lombok immutable classes and TableSchema.fromImmutableClass(), so this is a possible workaround.
klach-ocado commentedon Feb 10, 2026
Hi @debora-ito, I'm not sure what you mean that it works with regular non-Lombok immutable classes. The issue is about inconsistent handling of underscores, and without Lombok it looks as follows:
If
get__encryption_keyIdgetter fits AWS SDK's conventions, then builder's method should fit it too.The problem is located here:
... and later builder's method is not found because normalized getter does not have "get" prefix removed.
debora-ito commentedon Feb 17, 2026
@klach-ocado That is one of the restrictions for the non-lombok immutable classes to work, the getter and the setter must use the same method name - so it works if the getter is
public String __encryption_keyId()instead ofget__encryption_keyId(). Also,TableSchema.fromImmutableClass()must be used when creating the TableSchema. Other restrictions are documented in our Dev Guide - DynamoDB Enhanced - Work with immutable data classes.klach-ocado commentedon Feb 18, 2026
OK, thank you for clarification, I was not aware of the documented conventions for
@DynamoDbImmutable, moreover, I saw AWS examples of @DynamoDbImmutable with Lombok, and I thought getter needsgetprefix because it just works for regular names, and because of that other issue with Java records (but accessors' names are not the root cause there).Based on that documentation, it works for regular
getXyz()getters on a best effort basis. But looking at the source ofsoftware.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector#normalizeGetterName(quoted above), extending support for cases likeget_xyz()orget$xyz()should be simple, it may be enough to replace theCharacter.isUpperCase(...)conditions with something likeisUpperCaseOrNonAlphanumericCharacter(...)checking two things from its name.Regarding
TableSchema.fromImmutableClass(...), it calls the same code underneath thatTableSchema.fromClass(...)calls - the latter autodetects the@DynamoDbImmutableor@DynamoDbBeanannotation and calls a proper method depending on that.The issue is not a big priority for us, but considering potentially simple fix, it would be nice to fix that with some work in that area. Thanks.