1

I have a converter class that receives json in input, here are 2 valid examples:

{
  "method": "Model",
  "payload": {
    "key": "value"
  }
}

and

{
  "method": "OtherModel",
  "payload": {
    "foo": "bar"
  }
}

In C#, I have classes mapped to each possible model:

public class Model
{
  public string Key { get; set; }
}

public class OtherModel
{
  public string Foo { get; set; }
}

I need a generic converter

How can I use the string value in the method of the JSON to convert in a generic way the content of the payload field?

Is using a huge switch the only way? This is the prototype I have so far but there are hundreds of different models so it will grow quite large...

public IResult ParseJson(string json)
{
  Regex regexMessageName = new Regex("\"messageName\": \"(.*?)\"", RegexOptions.Compiled);
  var messageName = regexMessageName.Match(json).Groups[1].Value;
  switch (messageName)
  {
    case "Model":
      var raw = JsonConvert.DeserializeObject<JsonData<Model>>(json);
      return new LogInfoRequestResult<Model> { Raw = raw };
    case "OtherModel":
      var raw = JsonConvert.DeserializeObject<JsonData<OtherModel>>(json);
      return new LogInfoRequestResult<OtherModel> { Raw = raw };

  }
}
Bruno
  • 4,685
  • 7
  • 54
  • 105
  • I had a similar situation and this is how I solved it. In my case there where only about 40 or so cases. – Zohar Peled Aug 09 '19 at 14:03
  • 1
    What about deserializing `Payload` to a `Dictionary` ? – Cid Aug 09 '19 at 14:04
  • 1
    Are you looking for something like this: https://stackoverflow.com/a/3657466/10608418? You can search your assembly for a class with your `messageName` as name. Then you can use that to deserialize the json with the right model –  Aug 09 '19 at 14:13
  • @Cid hmm could be a solution...but I'd really prefer typed because the values are not always all strings – Bruno Aug 09 '19 at 14:24
  • @Knoop Thanks, yes something similar to this. I hope the performance would not be too bad with reflection though... – Bruno Aug 09 '19 at 14:24
  • @ibizia that would depend a bit. For starters you could create a dictionary with `messageName` as key and types you've already searched for. In case all the models share some common ancestor/interface or specific naming convention you could build that dictionary on start-up in one go –  Aug 09 '19 at 14:28
  • @Knoop And how do you instanciate a `new LogInfoRequestResult` from a type that is in a Dictionary ? – Bruno Aug 09 '19 at 14:37
  • 2
    @BrianRogers that would just move the switch to the converter... – Zohar Peled Aug 09 '19 at 14:38
  • @ibiza is the `method` always equal to the name of the class you want to instantiate? Or could it be different? – Brian Rogers Aug 09 '19 at 14:44
  • @BrianRogers I control the name of the C# class (model) so it can be the same (I think it is better to be) – Bruno Aug 09 '19 at 14:46
  • BTW, I hope you only need to handle the c# aspect of this. If you need to handle a json schema that describes these different objects, [the answer](https://stackoverflow.com/a/53884747/3094533) I've got to my question might help with that. – Zohar Peled Aug 09 '19 at 15:07

2 Answers2

4

If you want complete control of your classes, and allow them to evolve independently, then you can have one base class that owns the Method, and then as many subclasses as you want with their own definition of the payload.

First, parse into the baseclass, just to get a strongly typed deserialization of Method

Then, there are a lot of patterns to address branching logic.

  • If you have 1-2 cases, an if statement is fine
  • If you have 3-5 cases, you can use a switch
  • If you have 6-10 cases, you can create a dictionary that maps method name to class type
  • If you have more than that, you can use the strategy pattern and pass an interface around

Here's an example of how you could write the code:

var json = @"{
    'method': 'Model',
    'payload': {
        'key': 'value'
    }
}";

var modelBase = JsonConvert.DeserializeObject<ModelBase>(json);

var methodMapping = new Dictionary<string, Type>()
{
    {MethodTypes.Model.ToString(), typeof(Model)},
    {MethodTypes.OtherModel.ToString(), typeof(OtherModel)},
};

Type methodClass = methodMapping[modelBase.Method];

var result = JsonConvert.DeserializeObject(json, methodClass);

Note: Since we're programmatically determining the correct type, it's hard to pass to a generic <T>, so this uses the overload of DeserializeObject that takes type as a param

And here are the classes that model incoming messages

public enum MethodTypes
{
    Model,
    OtherModel
}

public class ModelBase
{
    public string Method { get; set; }
}

public class Model : ModelBase
{
    public ModelInfo Payload { get; set; }

    public class ModelInfo
    {
        public string Key { get; set; }
    }
}

public class OtherModel : ModelBase
{
    public ModelInfo Payload { get; set; }

    public class ModelInfo
    {
        public string Foo { get; set; }
    }
}

Dictionary<string,string>

If your data is always going to be "foo":"bar" or "key":"value" .... string:string, then Cid's suggesting to use Dictionary<string,string> Payload makes a lot of sense. Then figure out however you want to map from that c# class in a c# constructor that returns whatever type you want.

Additional Resources:

KyleMit
  • 30,350
  • 66
  • 462
  • 664
  • The enum gives you a way to independently name incoming method names with what class you want to map ti to. If they're always going to be in sync, you could just use `{nameof(Model), typeof(Model)}`, which will both be run at compile time, so should be pretty performant compared to a reflection lookup of class name. – KyleMit Aug 09 '19 at 15:25
2

You can instanciate an object of the expected class using Activator.CreateInstance(), then populate it with JsonConvert.PopulateObject()

In example :

Type t = Type.GetType($"NameSpaceName.{messageName}"); // this must be a fully qualified name
object obj = Activator.CreateInstance(t);
JsonConvert.PopulateObject(json, obj);
Cid
  • 14,968
  • 4
  • 30
  • 45