Skip to content

[BUG] Cosmos – CustomItemSerializer - not working in certain queries and not applied in SqlParameter #45521

@egroSK

Description

@egroSK

Describe the bug

I have a question and a potential bug related to the serialization and deserialization of data saved in JSON format in Cosmos DB using the Cosmos SDK. It relates to the customization of serialization/deserialization and the use of customItemSerializer.

Context

We have special requirements for how data should be serialized and deserialized to/from Cosmos DB. For example:

  • Serialize datetime types as ISO strings (disabling SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).
  • Deserialize datetime values without adjusting to the context timezone (disabling DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE).
  • Register subTypes to be used with @JsonTypeInfo, etc.

Historically (around 01/2022), the Cosmos SDK did not expose any public API for customizing serialization or the ObjectMapper. We found a workaround by reconfiguring the internal ObjectMapper (Utils.getSimpleObjectMapper()). We were aware this wasn’t the cleanest solution, but it worked.

With Cosmos SDK 4.66.1 (#44035), this hack became more complicated, as reconfiguration of another internal ObjectMapper, used by DefaultCosmosItemSerializer, and accessible only via the reflection API, is now also required.

We would like to finally get rid of this nasty hack and switch to using the customItemSerializer introduced in 4.59.0 (#38997).

Unfortunately, we have encountered issues with the usage of customItemSerializer that make it nearly unusable. I’d like to ask whether we are using/configuring it incorrectly, or if it is a bug in the Cosmos SDK.

Issue 1 – Deserialization errors in SELECT containing operations like SUM, ORDER BY, GROUP BY

The first issue occurs during deserialization when a SELECT used in CosmosAsyncContainer#queryItems includes operations like VALUE, aggregations (COUNT, GROUP BY, ...), ORDER BY, and similar. When customItemSerializer is configured, such queries fail during deserialization.

Code snippet with configuration, query, and exception
// Configuration
new CosmosClientBuilder()
    .customItemSerializer(new CosmosItemSerializer() {
    	// Prepare/inject configuredObjectMapper in constructor
        public <T> Map<String, Object> serialize(T item) { 
            return configuredObjectMapper.convertValue(item, Map.class); 
    	}
        public <T> T deserialize(Map<String, Object> jsonNodeMap, Class<T> classType) { 
            return configuredObjectMapper.convertValue(jsonNodeMap, classType); 
    	}
    })
    .buildAsyncClient();

// Select with ORDER BY
String query = "SELECT * FROM c ORDER BY c.id";
cosmosAsyncContainer.queryItems(new SqlQuerySpec(query), clazz);

// Exception
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "object" is null
    at com.azure.cosmos.implementation.query.orderbyquery.OrderByRowResult.getPayload(OrderByRowResult.java:43)
    at com.azure.cosmos.implementation.query.OrderByDocumentQueryExecutionContext$ItemToPageTransformer.lambda$apply$5(OrderByDocumentQueryExecutionContext.java:695)

As I have discovered, this is caused by the more complex way, how the deserialization internally works in Cosmos SDK:

  1. In the first step, the result from the database is deserialized into an internal object like Document (JsonSerializable), which has a structure that varies depending on the query. Usually, it contains not only the fields requested in the SELECT but also metadata used by the Cosmos SDK.
  2. In the second step, ValueUnwrapCosmosItemSerializer converts the data retrieved from the Document to the requested data type.
    • It also unwraps the VALUE if necessary.
    • Finally, it uses ObjectMapper to convert the value into the requested data type.

The problem is that customItemSerializer is called twice in these cases:

  • The first time is during an attempt to deserialize to Document (JsonSerializable), which does not work with the ObjectMapper. Cosmos SDK uses a special method, JsonSerializable.instantiateFromObjectNodeAndType, for this.
  • The second time is for the actual value that should be returned as the result to the application. This may also not work because the actual value might still be wrapped in a VALUE field. Cosmos SDK uses special behavior in ValueUnwrapCosmosItemSerializer to handle this.

I believe the custom serializer should be called only once, for the final value that is supposed to be returned as the query result to the application. It should not be called for internal Cosmos structures.

Issue 2 - SqlParameter serialization

The second issue relates to querying data from the database using CosmosAsyncContainer#queryItems and the serialization of parameters in SqlParameter. SqlParameter currently uses the internal Utils.simpleObjectMapper for serialization.

For example, when a datetime field is serialized (saved) as an ISO string in the database and filtering by this field is required, it is also necessary to serialize the SqlParameter as an ISO string. Otherwise, no results will be found, as LocalDate is by default serialized as an array.

Code snippet with a query example
String query = "SELECT * FROM c WHERE c.creationDate = @creationDate";
List<SqlParameter> params = List.of(new SqlParameter("@creationDate", LocalDate.of(2025, 5, 27)));
cosmosAsyncContainer.queryItems(new SqlQuerySpec(query, params), clazz);

What is the recommended way to handle this? Should the SDK apply customItemSerializer to SqlParameter serialization as well, or is it expected that SDK users manually serialize values (e.g., to strings) before passing them to SqlParameter? The latter approach is less user-friendly, as it requires manual step with serialization.

Exception or Stack Trace

Exceptions thrown when executing SELECT with operations like SUM, ORDER BY, etc., and with customItemSerializer configured, differ depending on the specific operation used in the query. One example of an exception for ORDER BY is shown in the section above (Issue 1 description). Exceptions for other queries are available in the reproducer project. Snippets with a few exception lines are included in the test comments, while more detailed output is available in the test report. The full stack trace can also be examined by running the tests in the reproducer project.

To Reproduce

Register a customItemSerializer (CosmosItemSerializer) to the CosmosAsyncClient, using the ObjectMapper#convertValue method for conversion. Then, execute a problematic query, for example, SELECT * FROM c ORDER BY c.id using cosmosAsyncContainer.queryItems. This will result in an exception during deserialization. For more details, see the code snippets in the Issue 1 and Issue 2 sections.

I have prepared a reproducer project with test cases that reflect the issues we encountered in our production code (queries) when attempting to switch from the hack involving reconfiguration of the internal ObjectMapper to using customItemSerializer.

Reproducer project:

  • CosmosConfig - configure ENDPOINT and KEY.
  • ReproducerTest - contains all test cases asserting our expected behavior.
    • Tests are categorized into two @Nested classes:
      • DateTimeSerialization - focuses on checking whether data is serialized as expected, indicating that the serializer/ObjectMapper is configured correctly.
      • Query - contains SELECTS that fail when customItemSerializer is used.
    • Each test includes a comment describing its purpose and how it behaves with different configuration options (see below).
    • Just run the tests and review the results. The goal for us would be to have all tests green (passing) for the BASIC_CUSTOM_SERIALIZER configuration option.
    • A test report is also available in test-report directory.

Tests can be executed with four possible configuration options:

  • NO_CUSTOMIZATION - plain CosmosAsyncClient without registration of customItemSerializer or reconfiguration of the internal ObjectMapper.
  • COSMOS_INTERNALS_RECONFIGURATION – no customItemSerializer registered. This is a hack involving reconfiguration of the internal Cosmos SDK ObjectMapper, which is the approach we are currently using.
  • BASIC_CUSTOM_SERIALIZERCosmosAsyncClient with registration of customItemSerializer. It uses ObjectMapper with required configuration for serialization/deserialization of data stored in Cosmos. However, it does not work as expected due to the issues described above.
  • UNWRAP_CUSTOM_SERIALIZERCosmosAsyncClient with registration of customItemSerializer. It uses an ObjectMapper configured for serialization/deserialization of data stored in Cosmos. Additionally, it implements a hack for deserializing internal JsonSerializable objects and unwrapping the VALUE field as a demonstration that such a hack could work. However, this approach is very fragile and relies on internal Cosmos classes, so it is definitely not the way to use it.

Expected behavior

  • customItemSerializer should be called only for the conversion of the final object returned as the result of the query, not for internal Cosmos SDK structures.
  • SqlParameter should probably use customItemSerializer for parameter serialization.

Setup:

  • OS: macOS 15.5 (local development environment) / Linux (production environment)
  • IDE: IntelliJ
  • Library/Libraries: com.azure:azure-cosmos:4.70.0
  • Java version: 21.0.7
  • App Server/Environment: - (not important for the issue to happen)
  • Frameworks: - (not important for the issue to happen)

Information Checklist
Kindly make sure that you have added all the following information above and checkoff the required fields otherwise we will treat the issuer as an incomplete report

  • Bug Description Added
  • Repro Steps Added
  • Setup information Added

Metadata

Metadata

Labels

ClientThis issue points to a problem in the data-plane of the library.CosmosService AttentionWorkflow: This issue is responsible by Azure service team.bugThis issue requires a change to an existing behavior in the product in order to be resolved.customer-reportedIssues that are reported by GitHub users external to the Azure organization.needs-team-attentionWorkflow: This issue needs attention from Azure service team or SDK team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions