Skip to content

Fields with underscores work with @DynamoDbBean but not with @DynamoDbImmutable #5589

Open

Description

@klach-ocado

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-enhanced version 2.27.19
  • org.springframework.boot:spring-boot-starter-test version 3.3.3
  • org.projectlombok:lombok version 1.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)

Activity

  1. added
    bugThis issue is a bug.
    needs-triageThis issue or PR still needs to be triaged.
    on Sep 12, 2024
  2. self-assigned this
    on Nov 25, 2024
  3. debora-ito commented on Feb 7, 2026

    @debora-ito
    Member

    @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.

  4. added
    third-partyThis issue is related to third-party libraries or applications.
    p2This is a standard priority issue
    and removed
    needs-triageThis issue or PR still needs to be triaged.
    on Feb 7, 2026
  5. klach-ocado commented on Feb 10, 2026

    @klach-ocado
    Author

    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:

    package com.example;
    
    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();
        }
    
        @DynamoDbBean
        public static class Bean {
    
            private String id;
            private String __encryption_keyId;
    
            @DynamoDbPartitionKey
            public String getId() {
                return this.id;
            }
    
            public String get__encryption_keyId() {
                return this.__encryption_keyId;
            }
    
            public void setId(String id) {
                this.id = id;
            }
    
            public void set__encryption_keyId(String __encryption_keyId) {
                this.__encryption_keyId = __encryption_keyId;
            }
        }
    
        @DynamoDbImmutable(builder = Immutable.ImmutableBuilder.class)
        public static final class Immutable {
    
            private final String id;
            private final String __encryption_keyId;
    
            Immutable(String id, String __encryption_keyId) {
                this.id = id;
                this.__encryption_keyId = __encryption_keyId;
            }
    
            @DynamoDbPartitionKey
            public String getId() {
                return this.id;
            }
    
            public String get__encryption_keyId() {
                return this.__encryption_keyId;
            }
    
            public static class ImmutableBuilder {
                private String id;
                private String __encryption_keyId;
    
                // let's have default public no-arg constructor, to eliminate potential problem caused by Lombok
    
                public ImmutableBuilder id(String id) {
                    this.id = id;
                    return this;
                }
    
                public ImmutableBuilder __encryption_keyId(String __encryption_keyId) {
                    this.__encryption_keyId = __encryption_keyId;
                    return this;
                }
    
                public Immutable build() {
                    return new Immutable(this.id, this.__encryption_keyId);
                }
            }
        }
    }
    

    If get__encryption_keyId getter fits AWS SDK's conventions, then builder's method should fit it too.

    The problem is located here:

    // software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector#normalizeGetterName
        private String normalizeGetterName(Method getter) {
            String getterName = getter.getName();
    
            if (getterName.length() > 2
                && Character.isUpperCase(getterName.charAt(2)) // <--- false for underscore
                && getterName.startsWith(IS_PREFIX)
                && isMethodBoolean(getter)) {
    
                return Character.toLowerCase(getterName.charAt(2)) + getterName.substring(3);
            }
    
            if (getterName.length() > 3
                && Character.isUpperCase(getterName.charAt(3)) // <--- false for underscore
                && getterName.startsWith(GET_PREFIX)) {
    
                return Character.toLowerCase(getterName.charAt(3)) + getterName.substring(4);
            }
    
            return getterName;
        }
    

    ... and later builder's method is not found because normalized getter does not have "get" prefix removed.

  6. debora-ito commented on Feb 17, 2026

    @debora-ito
    Member

    @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 of get__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.

  7. removed their assignment
    on Feb 17, 2026
  8. klach-ocado commented on Feb 18, 2026

    @klach-ocado
    Author

    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 needs get prefix 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 of software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableIntrospector#normalizeGetterName (quoted above), extending support for cases like get_xyz() or get$xyz() should be simple, it may be enough to replace the Character.isUpperCase(...) conditions with something like isUpperCaseOrNonAlphanumericCharacter(...) checking two things from its name.

    Regarding TableSchema.fromImmutableClass(...), it calls the same code underneath that TableSchema.fromClass(...) calls - the latter autodetects the @DynamoDbImmutable or @DynamoDbBean annotation 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.dynamodb-enhancedp2This is a standard priority issuethird-partyThis issue is related to third-party libraries or applications.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions