3636import com .fasterxml .jackson .core .JsonProcessingException ;
3737import com .fasterxml .jackson .databind .DeserializationFeature ;
3838import com .fasterxml .jackson .databind .ObjectMapper ;
39+ import com .fasterxml .jackson .databind .SerializationFeature ;
3940import com .fasterxml .jackson .databind .node .ObjectNode ;
41+ import com .fasterxml .jackson .datatype .jsr310 .JavaTimeModule ;
4042import org .testng .annotations .AfterClass ;
4143import org .testng .annotations .BeforeClass ;
4244import org .testng .annotations .DataProvider ;
@@ -93,7 +95,8 @@ public static Object[][] clientBuildersWithDirectSessionIncludeComputeGatewayAnd
9395 CosmosItemSerializer [] itemSerializers = new CosmosItemSerializer [] {
9496 null ,
9597 CosmosItemSerializer .DEFAULT_SERIALIZER ,
96- EnvelopWrappingItemSerializer .INSTANCE_NO_TRACKING_ID_VALIDATION
98+ EnvelopWrappingItemSerializer .INSTANCE_NO_TRACKING_ID_VALIDATION ,
99+ BasicCustomItemSerializer .INSTANCE
97100 };
98101
99102 List <Object []> providers = new ArrayList <>();
@@ -323,11 +326,11 @@ private <T> void runPointOperationAndQueryTestCase(
323326 Class <T > classType ) {
324327
325328 boolean useEnvelopeWrapper =
326- requestLevelSerializer == EnvelopWrappingItemSerializer . INSTANCE_NO_TRACKING_ID_VALIDATION
329+ requestLevelSerializer instanceof EnvelopWrappingItemSerializer
327330 || (requestLevelSerializer == null
328331 && this .getClientBuilder ()
329- .getCustomItemSerializer () == EnvelopWrappingItemSerializer . INSTANCE_NO_TRACKING_ID_VALIDATION );
330- if (requestLevelSerializer == EnvelopWrappingItemSerializer . INSTANCE_NO_TRACKING_ID_VALIDATION
332+ .getCustomItemSerializer () instanceof EnvelopWrappingItemSerializer );
333+ if (requestLevelSerializer instanceof EnvelopWrappingItemSerializer
331334 && isContentOnWriteEnabled
332335 && nonIdempotentWriteRetriesEnabled
333336 && useTrackingIdForCreateAndReplace ) {
@@ -704,6 +707,7 @@ public void queryWithOrderByAndCustomSerializer() {
704707 return ;
705708 }
706709
710+ boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer ;
707711 String pkValue = UUID .randomUUID ().toString ();
708712 List <String > createdIds = new ArrayList <>();
709713 try {
@@ -748,6 +752,7 @@ public void queryWithAggregateAndCustomSerializer() {
748752 return ;
749753 }
750754
755+ boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer ;
751756 String pkValue = UUID .randomUUID ().toString ();
752757 List <String > createdIds = new ArrayList <>();
753758 try {
@@ -760,14 +765,17 @@ public void queryWithAggregateAndCustomSerializer() {
760765 createdIds .add (id );
761766 }
762767
763- // Use DEFAULT_SERIALIZER for the query request options because aggregate
768+ // For envelope-wrapping serializer, use DEFAULT_SERIALIZER because aggregate
764769 // results (e.g., COUNT) are not full documents and cannot be deserialized
765- // by the envelope-wrapping serializer. The test still validates that the
766- // client-level custom serializer does not leak into the internal query pipeline.
767- // SELECT VALUE COUNT(1) returns a scalar integer, so use Integer.class .
770+ // by the envelope-wrapping serializer.
771+ // For BasicCustomItemSerializer, use the custom serializer directly since
772+ // it does not transform document structure .
768773 CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions ()
769- .setCustomItemSerializer (CosmosItemSerializer .DEFAULT_SERIALIZER );
774+ .setCustomItemSerializer (isEnvelopeWrapper
775+ ? CosmosItemSerializer .DEFAULT_SERIALIZER
776+ : clientSerializer );
770777
778+ // SELECT VALUE COUNT(1) returns a scalar integer, so use Integer.class.
771779 List <Integer > results = container
772780 .queryItems (
773781 "SELECT VALUE COUNT(1) FROM c WHERE c.mypk = '" + pkValue + "'" ,
@@ -794,6 +802,7 @@ public void queryWithDistinctAndCustomSerializer() {
794802 return ;
795803 }
796804
805+ boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer ;
797806 String pkValue = UUID .randomUUID ().toString ();
798807 List <String > createdIds = new ArrayList <>();
799808 try {
@@ -806,12 +815,14 @@ public void queryWithDistinctAndCustomSerializer() {
806815 createdIds .add (id );
807816 }
808817
809- // Use DEFAULT_SERIALIZER for the query request options because DISTINCT
818+ // For envelope-wrapping serializer, use DEFAULT_SERIALIZER because DISTINCT
810819 // projections are not full documents and cannot be deserialized by the
811- // envelope-wrapping serializer. The test still validates that the client-level
812- // custom serializer does not leak into the internal query pipeline .
820+ // envelope-wrapping serializer.
821+ // For BasicCustomItemSerializer, use the custom serializer directly .
813822 CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions ()
814- .setCustomItemSerializer (CosmosItemSerializer .DEFAULT_SERIALIZER );
823+ .setCustomItemSerializer (isEnvelopeWrapper
824+ ? CosmosItemSerializer .DEFAULT_SERIALIZER
825+ : clientSerializer );
815826
816827 List <ObjectNode > results = container
817828 .queryItems (
@@ -839,6 +850,7 @@ public void queryWithGroupByAndCustomSerializer() {
839850 return ;
840851 }
841852
853+ boolean isEnvelopeWrapper = clientSerializer instanceof EnvelopWrappingItemSerializer ;
842854 String pkValue = UUID .randomUUID ().toString ();
843855 List <String > createdIds = new ArrayList <>();
844856 try {
@@ -851,12 +863,14 @@ public void queryWithGroupByAndCustomSerializer() {
851863 createdIds .add (id );
852864 }
853865
854- // Use DEFAULT_SERIALIZER for the query request options because GROUP BY
866+ // For envelope-wrapping serializer, use DEFAULT_SERIALIZER because GROUP BY
855867 // projections are not full documents and cannot be deserialized by the
856- // envelope-wrapping serializer. The test still validates that the client-level
857- // custom serializer does not leak into the internal query pipeline .
868+ // envelope-wrapping serializer.
869+ // For BasicCustomItemSerializer, use the custom serializer directly .
858870 CosmosQueryRequestOptions queryRequestOptions = new CosmosQueryRequestOptions ()
859- .setCustomItemSerializer (CosmosItemSerializer .DEFAULT_SERIALIZER );
871+ .setCustomItemSerializer (isEnvelopeWrapper
872+ ? CosmosItemSerializer .DEFAULT_SERIALIZER
873+ : clientSerializer );
860874
861875 List <ObjectNode > results = container
862876 .queryItems (
@@ -1238,4 +1252,40 @@ public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) {
12381252 classType );
12391253 }
12401254 }
1255+
1256+ /**
1257+ * A simple custom item serializer that mirrors the real-world use case from
1258+ * <a href="https://github.com/Azure/azure-sdk-for-java/issues/45521">issue #45521</a>.
1259+ * Uses a custom ObjectMapper with different settings (e.g., dates as ISO strings)
1260+ * without transforming the document structure (no wrapping/unwrapping).
1261+ */
1262+ @ SuppressWarnings ("unchecked" )
1263+ private static class BasicCustomItemSerializer extends CosmosItemSerializer {
1264+ public static final BasicCustomItemSerializer INSTANCE = new BasicCustomItemSerializer ();
1265+
1266+ private static final ObjectMapper customMapper = new ObjectMapper ()
1267+ .registerModule (new JavaTimeModule ())
1268+ .configure (SerializationFeature .WRITE_DATES_AS_TIMESTAMPS , false )
1269+ .configure (DeserializationFeature .ADJUST_DATES_TO_CONTEXT_TIME_ZONE , false )
1270+ .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
1271+
1272+ private BasicCustomItemSerializer () {
1273+ }
1274+
1275+ @ Override
1276+ public <T > Map <String , Object > serialize (T item ) {
1277+ if (item == null ) {
1278+ return null ;
1279+ }
1280+ return customMapper .convertValue (item , Map .class );
1281+ }
1282+
1283+ @ Override
1284+ public <T > T deserialize (Map <String , Object > jsonNodeMap , Class <T > classType ) {
1285+ if (jsonNodeMap == null ) {
1286+ return null ;
1287+ }
1288+ return customMapper .convertValue (jsonNodeMap , classType );
1289+ }
1290+ }
12411291}
0 commit comments