0

I have a the following example classes

public class Item<TMessageType> where TMessageType : ItemMessage
{
  public int Prop1 { get; set; }

  public string Prop2 { get; set; }

  public int MessageType { get; set; }
    
  public TMessageType Message { get; set; }

}
    

public class ItemMessage
{
  public int SomeProperty { get; set; }
}
    
public class TypeAMessage: ItemMessage
{
  public string PropA { get; set; }
}
    
public class TypeBMessage: ItemMessage
{
  public string PropB { get; set; }
}
    
public class TypeCMessage: ItemMessage
{
  public string PropC { get; set; }
}

I will be receiving from a queue of 'Items' as JSON from an external feed and won't know the type of item message until it is received. I can successfully determine the message type via Regex on the raw JSON string and can use that in a switch statement to deserialise the item correctly. e.g.

// get type 
...

// deserialise
dynamic item;
switch(messageTypeFromJson)
{
  case 0:
    item = JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString);
    break;

  case 1:
    item = JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString);
    break;

  case 2:
    item = JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString);
    break;

  default:
    // handle unexpected type
    return;
}

// use item

The above code works but it feels messy. I would like to be able to do something closer to the following (which does not work) to split the determination of the type and the deserialisation into separate steps.

Type messageType;    
switch(messageTypeFromJson)
{
  case 0:
    messageType = typeof(TypeAMessage);
    break;

  case 1:
    messageType = typeof(TypeBMessage);
    break;

  case 2:
    messageType = typeof(TypeCMessage);
    break;

  default:
    // handle unexpected type
    return;
}

try
{
    var item = JsonSerializer.Deserialize<Item<messageType>>(jsonString);
    .....
}
catch(..){ ... }

Is there a way to achieve this?

Killian
  • 414
  • 2
  • 10
  • How do you plan to use `item` later? – Guru Stron Jul 04 '20 at 22:10
  • You can use reflection to make the call to `JsonSerializer.Deserialize<...>` with the right type from a variable, but you're not going to be able to "dynamically" declare the right type for `item`. Depending on how you intend to use this variable, the reflection method may or may not be an option. – Lasse V. Karlsen Jul 04 '20 at 22:11
  • @GuruStron the information in the `Item` class and especially within `Item.Message` contains bits of updated information about other domain objects within the system. So essentially for CRUD operations on a data store + meta/stats data. – Killian Jul 04 '20 at 22:20
  • Without actual code it is still hard for me to say, but if you are ok with multiple type checks you can introduce an interface with `object` for message type and declare the `item` typed with this interface (interface implementation can be explicit, if it is convenient). – Guru Stron Jul 04 '20 at 22:26
  • You can use the non-generic version of Deserialize passing in the type. Use the full generic type name in your case statement. `messageType = typeof(Item); var result = JsonSerializer.Deserialize(jsonString, messageType);` You can then either cast to a base type (if you can introduce one) or you can use pattern matching to work with it `if (result is Item ita) { /*...*/ }` You might also consider deserializing as a `JsonDocument` or `JsonElement` and working directly with the “raw“ json types. – pinkfloydx33 Jul 04 '20 at 22:36
  • 1
    There is a way to use generics with Type via reflection - `MakeGenericMethod` - but frankly your existing code is more efficient and cleaner than that. – Marc Gravell Jul 04 '20 at 22:39
  • Right but there's also non-generic versions of `Deserialize` that take a `Type` object. So it's kinda unnecessary (other than the fact the returned value will have a *static* type of `object`) – pinkfloydx33 Jul 04 '20 at 23:11

2 Answers2

0

Without a base class or interface on your Item<> class, the best you are going to be able to do is deal with objects.

First, use the full type name in your switch statement and not the generic parameter:

Type messageType;    
switch(messageTypeFromJson)
{
  case 0:
    messageType = typeof(Item<TypeAMessage>);
    break;
  case 1:
    messageType = typeof(Item<TypeBMessage>);
    break;
  case 2:
    messageType = typeof(Item<TypeCMessage>);
    break;
  default:
    // handle unexpected type
    return;
}

Then use the non-generic overload of Deserialize that expects the objects Type:

var item = JsonSerializer.Deserialize(jsonString, messageType);

Now it's important to note that without a base type/interface that you can't cast the return value to anything directly, leaving it as an object. However you can use pattern matching to work with the object:

if(item is Item<TypeAMessage> ita) 
{
   // Do something 
} 
else if (item is Item<TypeBMessage> itb)
{
   // etc
} 

Or since you know the type (remember the switch?) you could work with it like:

switch(messageTypeFromJson)
{
  case 0:
    Item<TypeAMessage> ita = (Item<TypeAMessage>)item;
    // Do something 
    break;
  case 1:
    // etc
    break;
}

Alternatively, you can deserialize as a JsonElement or JsonDocument instead and then work directly with the "raw" JSON types.

I'll note that the above is pretty repetitive. One alternative would look like this:

object item = messageTypeFromJson switch {
    0 => (object)JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString),
    1 => JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString), 
    2 => JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString), 
    _ => null
};

Then you can switch on the type (pattern matching) or on messageTypeFromJson to decide what you want to do with it.

It all depends on what you plan on doing with item--are you just returning it or do you need to access the properties on it? If it's just the former than you're good to go. If you need to access the properties you'll need a non-generic base type or interface.

public interface IItem
{
  public int Prop1 { get; set; }
  public string Prop2 { get; set; }
  public int MessageType { get; set; }
  public object Message { get; set; }
}

public interface IItem<TMessageType> : IItem where TMessageType : ItemMessage
{
  public new TMessageType Message { get; set; }
}

And then:

public class Item<TMessageType> : IItem<TMessageType> where TMessageType : ItemMessage
{
  public int Prop1 { get; set; }
  public string Prop2 { get; set; }
  public int MessageType { get; set; }
  object IItem.Message { get => Message; set => Message = value; } 
  public TMessageType Message { get; set; }
}

You'd then be able to use it like so:

var item = (IItem)JsonSerializer.Deserialize(jsonString, messageType);
// Do something with interface properties

Or similarly:

IItem item = messageTypeFromJson switch {
    0 => (IItem)JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString),
    1 => JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString), 
    2 => JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString), 
    _ => null
};

You could then switch on the type of Message instead of item:

if (item.Message is TypeAMessage tam)
{
   Console.WriteLine(tam.PropA);
   // etc
} 

Warning: do not use IItem as the generic type parameter (or the Type object) when calling Deserialize. Why? Because System.Text.Json will only deserialize the properties that exist on that type. That means you'd lose the strongly-type Message data and some of it's properties.

In other words, don't do this:

IItem item = JsonSerializer.Deserialize<IItem>(jsonString);
pinkfloydx33
  • 11,863
  • 3
  • 46
  • 63
  • There's some ways to get fancy using a couple dictionaries that store some delegates but it's a bit overkill here and you'd basically be repeating everything anyways since there's no common (non-generic) base class to work with – pinkfloydx33 Jul 04 '20 at 22:49
0

It's not recommended to use reflection.

Here is the code:

       //get generic deserialize method (there is two, we should filter to get the one we need.
        var deserializeMethod = typeof(JsonConvert).GetMethods().
            Where(x =>x.Name == nameof(JsonConvert.DeserializeObject)).
            Where(x =>x.IsGenericMethod).FirstOrDefault();

        var itemType = typeof(List<>/*item type here*/);
        var listGenericType = itemType.MakeGenericType(typeof(int)/*type from switch here*/);

        var genericMethod = deserializeMethod.MakeGenericMethod(listGenericType);

        var mappedObject = genericMethod.Invoke(null, new object[] { "[1,2,3]" /*your JSON here*/ });

Your way is more readable and more safe.

You can just make Generic Type instance and methods call in this way when using GetType() method or typeof() keyword.

Hassan Monjezi
  • 1,060
  • 1
  • 8
  • 24