Search code examples
c#.netjson.netmasstransit

How to avoid incompatible during deserializing polymorphic properties in MassTransit?


When using the request/response pattern, during the deserialization of the response, an incompatibility error occurs. Apparently, the publisher serializer configuration is causing this issue, although the message is as expected.

Using the JsonProperty feature could isolate the problem, however, it does not reflect what was expected.

Versions .NET: 6.0; MassTRansit: 7.3.0; Newtonsoft: 13.0.1

Type specified in JSON 'Messages.Models+CreditCard, Messages' 
is not compatible with 'GreenPipes.DynamicInternal.Models\+Messages.Models\+IPaymentMethod'

Resource:

_bus.CreateRequestClient<TMessage>().GetResponse<TResponse>(message, cancellationToken);

Publisher serializer configuration:

bus.ConfigureJsonSerializer(settings =>
{
    settings.TypeNameHandling = TypeNameHandling.Objects;
    return settings;
});

Message response serialization:

{
  "message": {
    "$type": "Messages.Services.ShoppingCarts.Responses+CartDetails, Messages",
    "cartItems": [
      {
        "$type": "Messages.Models+Item, Messages",
        "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productName": "string",
        "unitPrice": "10.0",
        "quantity": 30,
        "pictureUrl": "string"
      }
    ],
    "paymentMethods": [
      {
        "$type": "Messages.Models+CreditCard, Messages",
        "id": "be7c40ac-1cd1-4e35-bccf-a1a2f4efecfd",
        "expiration": "01/22",
        "holderName": "string",
        "number": "374245455400126",
        "securityNumber": "string"
      },
      {
        "$type": "Messages.Models+PayPal, Messages",
        "id": "9465cf12-a322-477d-94c8-116d03a8399e",
        "Password": "123",
        "UserName": "string"
      }
    ],

    ...

  }
}

Consumer deserializer configuration:

bus.ConfigureJsonDeserializer(settings =>
{
    settings.TypeNameHandling = TypeNameHandling.Objects; // or Auto, All, etc...
    return settings;
});

Error:

System.Runtime.Serialization.SerializationException: A JSON serialization exception occurred while deserializing the message envelope
    ---> Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'Messages.Models+CreditCard, Messages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 
    is not compatible with 'GreenPipes.DynamicInternal.Models\+Messages.Models\+IPaymentMethod, MessagesGreenPipes.DynamicInternale7ccc67139ad479db488c4fa6310335a, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. 
    Path 'message.paymentMethods[0].$type', line 30, position 55.

JsonProperty does not work:

public record CartDetails : Response
{
    [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public IEnumerable<Models.IPaymentMethod> PaymentMethods { get; init; }
}
{
  "message": {
    "cartItems": [
      {
        "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productName": "string",
        "unitPrice": "10.0",
        "quantity": 30,
        "pictureUrl": "string"
      }
    ],
    "paymentMethods": [
      {
        "id": "be7c40ac-1cd1-4e35-bccf-a1a2f4efecfd",
        "expiration": "01/22",
        "holderName": "string",
        "number": "374245455400126",
        "securityNumber": "string"
      },
      {
        "id": "9465cf12-a322-477d-94c8-116d03a8399e",
        "Password": "123",
        "UserName": "string"
      }
    ],

    ...

  }
}

Response

public record CartDetails : Response
{
    // [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public IEnumerable<Models.IPaymentMethod> PaymentMethods { get; init; }
    public IEnumerable<Models.Item> CartItems { get; init; }
    public Guid UserId { get; init; }
    public decimal Total { get; init; }
    public Guid Id { get; init; }
    public bool IsDeleted { get; init; }
}

Types:

public interface IPaymentMethod
{
    Guid Id { get; }
    decimal Amount { get; }
}

public record CreditCard : IPaymentMethod
{
    public Guid Id { get; init; }
    public decimal Amount { get; init; }
    [property: JsonConverter(typeof(ExpirationDateOnlyJsonConverter))]
    public DateOnly Expiration { get; init; }
    public string HolderName { get; init; }
    public string Number { get; init; }
    public string SecurityNumber { get; init; }
}

public record PayPal : IPaymentMethod
{
    public Guid Id { get; init; }
    public decimal Amount { get; init; }
    public string UserName { get; init; }
    public string Password { get; init; }
}

Solution

  • Thanks Wiktor Zychla, the issue led me to Nkot's workaround, which served me very well:

    TypeNameHandlingConverter:

    internal class TypeNameHandlingConverter : JsonConverter
    {
        private readonly TypeNameHandling _typeNameHandling;
    
        public TypeNameHandlingConverter(TypeNameHandling typeNameHandling)
        {
            _typeNameHandling = typeNameHandling;
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
            => new JsonSerializer { TypeNameHandling = _typeNameHandling }.Serialize(writer, value);
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
            => new JsonSerializer { TypeNameHandling = _typeNameHandling }.Deserialize(reader, objectType);
    
        public override bool CanConvert(Type objectType) 
            => IsMassTransitOrSystemType(objectType) is false;
    
        private static bool IsMassTransitOrSystemType(Type objectType)
            => objectType.Assembly == typeof(IConsumer).Assembly ||
               objectType.Assembly.IsDynamic ||
               objectType.Assembly == typeof(object).Assembly;
    }
    

    Consumer deserializer configuration:

    bus.ConfigureJsonDeserializer(settings =>
    {
        settings.Converters.Add(new TypeNameHandlingConverter(TypeNameHandling.Objects));
        return settings;
    });