0

Scenario :

  • I have a cosmosDb container stored in Azure.

  • I have this kind of data class that I read and write to it :

    public class MyClass {
         public Guid Id { get; private set; }
         public string PartitionKey { get; private set; }
    
         [JsonConverter(typeof(MyTypeJsonConverter))]
         //[Newtonsoft.Json.JsonConverter(typeof(MyTypeNewtonsoftJsonConverter))]
         public MyType Custom { get; private set; }
    
         [JsonConstructor] // <-- I've tried with and without this
         public MyClass(Guid id, string pk, MyType custom) {
              Custom = custom; Id = id; PartitionKey = pk;
         }
    }
    
  • as you can see there's a custom type, MyType, that gets converted with a custom converter.

  • For test purposes, I wrote the converter both with Newtonsoft and System.Text:

    public class MyTypeJsonConverter : JsonConverter<MyType> {
    
         public override bool CanConvert(Type objectType) { ... }
         public override void Write(...) { ... } 
         public override Context Read(...) { ... } 
    }
    
    public class ContextNewtonsoftJsonConverter : Newtonsoft.Json.JsonConverter {
    
         ... 
    }
    
  • I know that MyType works and that the converters work because serialization and deserialization work as expected both with System.Text.Json and Newtonsoft.Json when I do just this :

      Newtonsoft
      //var myClass = JsonConvert.DeserializeObject<MyClass>(someJson);
    
      //System.Text.Json
      var myClass2 =
         JsonSerializer.Deserialize<MyClass>(someJson,
            new JsonSerializerOptions()
               { IgnoreNullValues = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
    
  • A similar deserialization happens too when I read and write objects from CosmosDb.

         CosmosClientOptions options = new()
         {
               ConnectionMode = DebugHelper.DebugMode ? 
                     ConnectionMode.Gateway : ConnectionMode.Direct,
               SerializerOptions = new CosmosSerializationOptions
               {
               IgnoreNullValues = false,
               PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
               },
         };
         var cosmosClient = CosmosClient
                     .CreateAndInitializeAsync(connectionString, 
                           containersList, options)
                     .GetAwaiter()
                     .GetResult();
    
         var container = cosmosClient .GetContainer("mydatabase", "mycontainer");
    
    
         var items = container.GetItemLinqQueryable<MyType>(allowSynchronousQueryExecution: true);
         foreach (var item in items)
         {
               await container.DeleteItemAsync<MyType>(item.Id.ToString(), new PartitionKey(item.PartitionKey));
         }
    

Problem :

the code just above works perfectly with the NewtonSoft version... ...But fails with the System.Text.Json version.

---- Newtonsoft.Json.JsonSerializationException : Error converting value "my string value" to type 'MyType'. -------- System.ArgumentException : Could not cast or convert from System.String to MyType.

That exception does NOT happen inside the Read and Write functons of the converter. It happens "beforehand". It's like the [JsonConverter(...)] attribute is understood by JsonSerializer.Deserialize<Mytype> but not by cosmosContainer.DoSomethingWithItem<MyType> .

Again; it works with a plain serialization/desrialization, and it works with the CosmosClient when I use Newtonsoft.

Question :

  • (before you dive into complex Json) Can you spot an obvious mistake?
  • Do I need to register the custom JsonConverter, and if yes what's the most compact way of doing that? (Anything that doesn't require me to implement an entire custom Serializer to pass to CosmosDb... unless you can prove to me that there's a good explanation why Newtonsoft can do it without it but System.Text.Json can't)
jeancallisti
  • 1,046
  • 1
  • 11
  • 21
  • adding an implicit operator fixes the issue but I feel like it's a dirty hack : `public static implicit operator Context(string s) { return new Context(s); }` It's somewhat redundant to the Json converter. Or is it not? – jeancallisti May 10 '23 at 13:56

1 Answers1

2

Yes, you can (and should, in your case) register a custom Serializer:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;

namespace FooBar
{
    // Custom implementation of CosmosSerializer which works with System.Text.Json instead of Json.NET.
    // https://github.com/Azure/azure-cosmos-dotnet-v3/issues/202
    // This is temporary, until CosmosDB SDK v4 is available, which should remove the Json.NET dependency.
    public class CosmosNetSerializer : CosmosSerializer
    {
        private readonly JsonSerializerOptions _serializerOptions;

        public CosmosNetSerializer() => this._serializerOptions = null;

        public CosmosNetSerializer(JsonSerializerOptions serializerOptions) => this._serializerOptions = serializerOptions;

        public override T FromStream<T>(Stream stream)
        {
            using (stream)
            {
                if (typeof(Stream).IsAssignableFrom(typeof(T)))
                {
                    return (T)(object)stream;
                }

                return JsonSerializer.DeserializeAsync<T>(stream, this._serializerOptions).GetAwaiter().GetResult();
            }
        }

        public override Stream ToStream<T>(T input)
        {
            var outputStream = new MemoryStream();

            //TODO: replace with sync variant too?
            JsonSerializer.SerializeAsync<T>(outputStream, input, this._serializerOptions).GetAwaiter().GetResult();

            outputStream.Position = 0;
            return outputStream;
        }
    }
}
CosmosClientBuilder clientBuilder = new CosmosClientBuilder(sysConfig.CosmosEndpointUri, tokenCredential)
  .WithConnectionModeDirect()
// ... With()...
  .WithCustomSerializer(new CosmosNetSerializer(Globals.JsonSerializerOptions))

Full example here

silent
  • 14,494
  • 4
  • 46
  • 86
  • *sad noises* ... Would you be able to point out where the cosmosclient is configured to use Newtonsoft? I think that's my root problm there : it defaults to Newtonsoft without asking me. Is it only in the client builder or can it be somewhere else? – jeancallisti May 10 '23 at 14:05
  • 1
    In my solution it is configured to use STJ, instead of Newtonsoft (which is the built-in default) – silent May 10 '23 at 14:09
  • 1
    Oh I only just understood that Json.Net is not a "choice" but a forced implementation until a more recent CosmosDb. ...And here I was, proud of having removed all dependencies to it in my own code. – jeancallisti May 10 '23 at 14:11
  • 1
    Well, if you follow my example, you can switch to STJ... – silent May 10 '23 at 14:48