1919import static software .amazon .awssdk .enhanced .dynamodb .mapper .StaticAttributeTags .primaryPartitionKey ;
2020
2121import java .time .Instant ;
22+ import java .util .ArrayList ;
2223import java .util .Collections ;
24+ import java .util .HashMap ;
2325import java .util .List ;
2426import java .util .Map ;
2527import java .util .Set ;
28+ import java .util .stream .Collectors ;
29+ import software .amazon .awssdk .enhanced .dynamodb .AttributeConverter ;
30+ import software .amazon .awssdk .enhanced .dynamodb .AttributeValueType ;
2631import software .amazon .awssdk .enhanced .dynamodb .EnhancedType ;
2732import software .amazon .awssdk .enhanced .dynamodb .TableSchema ;
2833import software .amazon .awssdk .enhanced .dynamodb .extensions .annotations .DynamoDbAutoGeneratedTimestampAttribute ;
2934import software .amazon .awssdk .enhanced .dynamodb .mapper .StaticImmutableTableSchema ;
3035import software .amazon .awssdk .enhanced .dynamodb .mapper .StaticTableSchema ;
3136import software .amazon .awssdk .enhanced .dynamodb .mapper .annotations .DynamoDbBean ;
37+ import software .amazon .awssdk .enhanced .dynamodb .mapper .annotations .DynamoDbConvertedBy ;
3238import software .amazon .awssdk .enhanced .dynamodb .mapper .annotations .DynamoDbImmutable ;
3339import software .amazon .awssdk .enhanced .dynamodb .mapper .annotations .DynamoDbPartitionKey ;
40+ import software .amazon .awssdk .services .dynamodb .model .AttributeValue ;
3441
3542/**
3643 * Test models specifically designed for auto-generated timestamp functionality testing. These models focus on the "time"
@@ -817,4 +824,227 @@ public BeanWithInvalidNestedAttributeNameChild setAttr_NESTED_ATTR_UPDATE_(Insta
817824 }
818825 }
819826 }
827+
828+ /**
829+ * Plain POJO with no DynamoDB annotations. Serialization is handled entirely by
830+ * {@link CustomConvertedPojoListConverter}.
831+ */
832+ public static class CustomConvertedPojo {
833+ private String label ;
834+ private int count ;
835+
836+ public CustomConvertedPojo () {
837+ }
838+
839+ public CustomConvertedPojo (String label , int count ) {
840+ this .label = label ;
841+ this .count = count ;
842+ }
843+
844+ public String getLabel () {
845+ return label ;
846+ }
847+
848+ public void setLabel (String label ) {
849+ this .label = label ;
850+ }
851+
852+ public int getCount () {
853+ return count ;
854+ }
855+
856+ public void setCount (int count ) {
857+ this .count = count ;
858+ }
859+ }
860+
861+ /**
862+ * Custom converter for {@code List<CustomConvertedPojo>}. The SDK should not attempt to resolve a
863+ * {@link TableSchema} for {@link CustomConvertedPojo} when this converter is present.
864+ */
865+ public static class CustomConvertedPojoListConverter implements AttributeConverter <List <CustomConvertedPojo >> {
866+
867+ @ Override
868+ public AttributeValue transformFrom (List <CustomConvertedPojo > input ) {
869+ if (input == null ) {
870+ return AttributeValue .builder ().nul (true ).build ();
871+ }
872+ List <AttributeValue > items = input .stream ()
873+ .map (pojo -> {
874+ Map <String , AttributeValue > map = new HashMap <>();
875+ map .put ("label" , AttributeValue .builder ().s (pojo .getLabel ()).build ());
876+ map .put ("count" , AttributeValue .builder ().n (String .valueOf (pojo .getCount ())).build ());
877+ return AttributeValue .builder ().m (map ).build ();
878+ })
879+ .collect (Collectors .toList ());
880+ return AttributeValue .builder ().l (items ).build ();
881+ }
882+
883+ @ Override
884+ public List <CustomConvertedPojo > transformTo (AttributeValue input ) {
885+ if (input .l () == null ) {
886+ return new ArrayList <>();
887+ }
888+ return input .l ().stream ()
889+ .map (av -> {
890+ Map <String , AttributeValue > m = av .m ();
891+ CustomConvertedPojo pojo = new CustomConvertedPojo ();
892+ if (m .containsKey ("label" )) {
893+ pojo .setLabel (m .get ("label" ).s ());
894+ }
895+ if (m .containsKey ("count" )) {
896+ pojo .setCount (Integer .parseInt (m .get ("count" ).n ()));
897+ }
898+ return pojo ;
899+ })
900+ .collect (Collectors .toList ());
901+ }
902+
903+ @ Override
904+ public EnhancedType <List <CustomConvertedPojo >> type () {
905+ return EnhancedType .listOf (CustomConvertedPojo .class );
906+ }
907+
908+ @ Override
909+ public AttributeValueType attributeValueType () {
910+ return AttributeValueType .L ;
911+ }
912+ }
913+
914+ /**
915+ * Bean that combines {@code @DynamoDbAutoGeneratedTimestampAttribute} with a
916+ * {@code @DynamoDbConvertedBy} list whose element type ({@link CustomConvertedPojo}) has no DynamoDB
917+ * annotations. Reproduces the regression described in GitHub issue #6852.
918+ */
919+ @ DynamoDbBean
920+ public static class BeanWithCustomConvertedList {
921+ private String id ;
922+ private Instant time ;
923+ private List <CustomConvertedPojo > customItems ;
924+
925+ @ DynamoDbPartitionKey
926+ public String getId () {
927+ return id ;
928+ }
929+
930+ public BeanWithCustomConvertedList setId (String id ) {
931+ this .id = id ;
932+ return this ;
933+ }
934+
935+ @ DynamoDbAutoGeneratedTimestampAttribute
936+ public Instant getTime () {
937+ return time ;
938+ }
939+
940+ public BeanWithCustomConvertedList setTime (Instant time ) {
941+ this .time = time ;
942+ return this ;
943+ }
944+
945+ @ DynamoDbConvertedBy (CustomConvertedPojoListConverter .class )
946+ public List <CustomConvertedPojo > getCustomItems () {
947+ return customItems ;
948+ }
949+
950+ public BeanWithCustomConvertedList setCustomItems (List <CustomConvertedPojo > customItems ) {
951+ this .customItems = customItems ;
952+ return this ;
953+ }
954+ }
955+
956+ /**
957+ * Custom converter for {@code Map<String, CustomConvertedPojo>}. Serializes the map as a DynamoDB M attribute
958+ * whose values are themselves maps. The SDK should not attempt to resolve a {@link TableSchema} for
959+ * {@link CustomConvertedPojo} when this converter is present.
960+ */
961+ public static class CustomConvertedPojoMapConverter implements AttributeConverter <Map <String , CustomConvertedPojo >> {
962+
963+ @ Override
964+ public AttributeValue transformFrom (Map <String , CustomConvertedPojo > input ) {
965+ if (input == null ) {
966+ return AttributeValue .builder ().nul (true ).build ();
967+ }
968+ Map <String , AttributeValue > map = new HashMap <>();
969+ for (Map .Entry <String , CustomConvertedPojo > entry : input .entrySet ()) {
970+ Map <String , AttributeValue > inner = new HashMap <>();
971+ inner .put ("label" , AttributeValue .builder ().s (entry .getValue ().getLabel ()).build ());
972+ inner .put ("count" , AttributeValue .builder ().n (String .valueOf (entry .getValue ().getCount ())).build ());
973+ map .put (entry .getKey (), AttributeValue .builder ().m (inner ).build ());
974+ }
975+ return AttributeValue .builder ().m (map ).build ();
976+ }
977+
978+ @ Override
979+ public Map <String , CustomConvertedPojo > transformTo (AttributeValue input ) {
980+ if (input .m () == null ) {
981+ return new HashMap <>();
982+ }
983+ Map <String , CustomConvertedPojo > result = new HashMap <>();
984+ for (Map .Entry <String , AttributeValue > entry : input .m ().entrySet ()) {
985+ Map <String , AttributeValue > m = entry .getValue ().m ();
986+ CustomConvertedPojo pojo = new CustomConvertedPojo ();
987+ if (m .containsKey ("label" )) {
988+ pojo .setLabel (m .get ("label" ).s ());
989+ }
990+ if (m .containsKey ("count" )) {
991+ pojo .setCount (Integer .parseInt (m .get ("count" ).n ()));
992+ }
993+ result .put (entry .getKey (), pojo );
994+ }
995+ return result ;
996+ }
997+
998+ @ Override
999+ public EnhancedType <Map <String , CustomConvertedPojo >> type () {
1000+ return EnhancedType .mapOf (String .class , CustomConvertedPojo .class );
1001+ }
1002+
1003+ @ Override
1004+ public AttributeValueType attributeValueType () {
1005+ return AttributeValueType .M ;
1006+ }
1007+ }
1008+
1009+ /**
1010+ * Bean that combines {@code @DynamoDbAutoGeneratedTimestampAttribute} with a
1011+ * {@code @DynamoDbConvertedBy} map whose value type ({@link CustomConvertedPojo}) has no DynamoDB
1012+ * annotations. Verifies the map path is safe alongside the list regression test.
1013+ */
1014+ @ DynamoDbBean
1015+ public static class BeanWithCustomConvertedMap {
1016+ private String id ;
1017+ private Instant time ;
1018+ private Map <String , CustomConvertedPojo > customMap ;
1019+
1020+ @ DynamoDbPartitionKey
1021+ public String getId () {
1022+ return id ;
1023+ }
1024+
1025+ public BeanWithCustomConvertedMap setId (String id ) {
1026+ this .id = id ;
1027+ return this ;
1028+ }
1029+
1030+ @ DynamoDbAutoGeneratedTimestampAttribute
1031+ public Instant getTime () {
1032+ return time ;
1033+ }
1034+
1035+ public BeanWithCustomConvertedMap setTime (Instant time ) {
1036+ this .time = time ;
1037+ return this ;
1038+ }
1039+
1040+ @ DynamoDbConvertedBy (CustomConvertedPojoMapConverter .class )
1041+ public Map <String , CustomConvertedPojo > getCustomMap () {
1042+ return customMap ;
1043+ }
1044+
1045+ public BeanWithCustomConvertedMap setCustomMap (Map <String , CustomConvertedPojo > customMap ) {
1046+ this .customMap = customMap ;
1047+ return this ;
1048+ }
1049+ }
8201050}
0 commit comments