Search code examples
c#azure-cosmosdbsystem.text.jsonc#-10.0

System.Text.Json custom serializer for CosmosDb serialises all fields... except id


.Net 6, C#10

This is my classes hierarchy:

public interface IWithKey {
   public Guid Id {get; }
}

public interface IType : IWithKey {
   public string Payload { get; }
   // a bunch of other complex fields (strings, Guids, nested objects...)
}

public class MyBaseType : IWithKey {

   [JsonInclude]
   public Guid Id {get; protected set; }


   public MyBaseType(Guid id) {
       Id = id;
   }
}

public class MyType : MyBaseType, IType {
   public Guid Id { get; private set; }
   public string Payload { get; private set; }
   // a bunch of other complex fields (strings, Guids, nested objects...)

   public MyType(Guid id, string payload, /* all the other fields... */)
   : base(id)
  {
        Payload = payload;
        // all the other fields...
   }
}

I have a custom Cosmos serializer. It exists solely to let me use system.Text.Json instead of Newtonsoft's Json.Net (as explained here: https://github.com/Azure/azure-cosmos-dotnet-v3/issues/202 )

   using System.Text.Json;
   using Microsoft.Azure.Cosmos;


   public class CosmosNetSerializer : CosmosSerializer
   {
      private readonly JsonSerializerOptions? _serializerOptions;

      public CosmosNetSerializer() => _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, _serializerOptions).GetAwaiter().GetResult();
         }
      }

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

         // A BREAKPOINT HERE SHOWS THAT 'input' has all the fields, including Id

         JsonSerializer.SerializeAsync<T>(outputStream, input, _serializerOptions).GetAwaiter().GetResult();

         // A BREAKPOINT HERE AND A LOOK INSIDE 'outputStream' shows that ALL the fields have been serialized EXCEPT 'Id' or 'id.

         outputStream.Position = 0;
         return outputStream;
      }
   }
}

Not that it matters, but the serializer is instantiated this way :

     var options = new(JsonSerializerDefaults.Web);
     var cosmosOptions = new CosmosClientOptions
     {
        ConnectionMode = //...,
        Serializer = new CosmosNetSerializer(options)
     };

     var cosmosClient = CosmosClient
           .CreateAndInitializeAsync(connectionString, containersList, cosmosOptions);

     

The serialization is triggered by inserting into Cosmos :

     var container = cosmosClient.GetContainer("myDatabase", "myContainer");

     var item = GetItem();
     await container.CreateItemAsync(item, "partitionKey", null);

Please note the subtlety : It lies in how the item is obtained. It's not of type MyType but of type IType :

public IType GetItem() {
    return new MyType(id : Guid.NewGuid(), payload: "YYYY", /* all the other fields... */)
}

Problem:

Look again at the comments in the serializer's code. input is a full-blown object of type IType (passed as IType but the breakpoint shows that C# understands that, in effect, it's a MyType). It does have a field "Id" with a valid value, and a TON of other custom fields. Some of them with custom converters, some with nested objects, etc.

Absolutely everything gets serialized, I can see it in outputStream... ...Except for 'Id' or 'id', which goes missing.

Why?


Solution

  • The issue was caused by the class hierarchy. Id was the only field coming from an underlying class, and System.Text.Json requires extra wotk to handle serialization of class hierarchies.

    I changed this : SerializeAsync<T>

    to this : SerializeAsync<object>

    ...as suggested in some posts that explain how to handle class hierarchies in .Net6 with System.Text.Json.

    If I had been using .Net7 then there exist "better" solutions where you indicate the class hierarchy with decorators above each class definition.