Azure CosmosDb logo

System.Text.Json.JsonPropertyName not working for CosmosDb in .NET Core/5?

3 min read.

This article explains how to fix an annoying issue with Microsoft’s SDK for CosmosDb v3 – it comes with a Newtonsoft.Json dependency, that most of Microsoft’s recent packages have let go of. With .NET Core 3.1 having shipped with System.Text.Json included, and (mostly) replacing Newtonsoft.Json, it’s kind of the preferred option.

However, CosmosDb v3 SDK doesn’t support it and by default requires you to use Newtonsoft.Json to override the property names, if you want to – for example – map entities with PascalCase naming with data in CosmosDb that’s using camelCase.

This is described on GitHub for CosmosDb SDK as such:

.NET v3 SDK was released before System.Text.Json was GA. Switching the entire SDK over from newtonsoft to System.Text.Json is a breaking change which would require a v4 SDK.

https://github.com/Azure/azure-cosmos-dotnet-v3/issues/2533

So until v4, Newtonsoft it is. Or is it?

Problem

Let’s take a quick step back and see what was the issue again. When you create a model somewhat like this:

You’ll get thrown NullReferenceExceptions or similar, because your DeviceId and UserId are null – even though there’s data in the database!

Oh, but CosmosDb uses camelCase instead of PascalCase, so obviously these properties won’t get mapped to anything. To fix this, you add the JsonPropertyName-attribute to map the data in your CosmosDb containers to your C# model:

But the problem persists – it’s almost as if the JsonPropertyName -attribute isn’t applied at all!

And similarly, renaming your properties to camelCase fixes the issue (but feels wrong).

You could also add a reference to Newtonsoft.Json – but if you don’t want to do that either, there are other things you can do.

Solution

CosmosDb SDK v3 needs a quick additional step to make it understand the “new” System.Text.Json.JsonPropertyName. Let’s take a look!

Solution / Workaround

Time needed: 30 minutes.

How to configure JSON casing for CosmosDb (v3)?

  1. Add the JsonPropertyName attributes

    You can match pretty much any property in your C# class to whatever property in the container – but traditionally it does look somewhat like this:

    [JsonPropertyName("deviceId")]
    public string DeviceId{ get; set; }


    And probably you need to add this as well:

    using System.Text.Json.Serialization;

  2. Create a new class for serialization

    This class takes care of the actual serialization, because like hinted before, CosmosDb (v3 SDK) doesn’t know how to do it with System.TextJson. The whole sample is shown in appendix 1.

  3. Configure your CosmosClient with this “new” serializer

    Wherever you instantiate or configure your CosmosClient, pass this CosmosClientOptions object to it and you should be good!

    JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
    {
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };
    CosmosSystemTextJsonSerializer cosmosSystemTextJsonSerializer
    = new CosmosSystemTextJsonSerializer(jsonSerializerOptions);
    CosmosClientOptions cosmosClientOptions = new CosmosClientOptions()
    {
    Serializer = cosmosSystemTextJsonSerializer
    };

    Like so:

    _client = new Microsoft.Azure.Cosmos.CosmosClient(account, key, cosmosClientOptions);

  4. And voilà! You should be good 😎

    Now your mapping should work just fine.

References and appendices

Appendix 1

namespace Contoso.Shared
{
    using System.IO;
    using System.Text.Json;
    using Azure.Core.Serialization;
    using Microsoft.Azure.Cosmos;

    public class CosmosSystemTextJsonSerializer : CosmosSerializer
    {
        private readonly JsonObjectSerializer systemTextJsonSerializer;

        public CosmosSystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions)
        {
            this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
        }

        public override T FromStream<T>(Stream stream)
        {
            using (stream)
            {
                if (stream.CanSeek
                       && stream.Length == 0)
                {
                    return default;
                }

                if (typeof(Stream).IsAssignableFrom(typeof(T)))
                {
                    return (T)(object)stream;
                }

                return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default);
            }
        }

        public override Stream ToStream<T>(T input)
        {
            MemoryStream streamPayload = new MemoryStream();
            this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default);
            streamPayload.Position = 0;
            return streamPayload;
        }
    }
}
mm
5 1 vote
Article Rating
Subscribe
Notify of
guest
2 Comments
most voted
newest oldest
Inline Feedbacks
View all comments