Search code examples
c#serializationjsonconvert

Using JsonConvert.DeserializeObject to dynamically choose class


So I am making an api call and I need to use JsonConvert.DeserializeObject to convert it to a class. The Json structure comes back as the following

{
    "fcResponse": {
        "responseData": {
            "fcRequest": {
                "mail": "Emails",
                "outlookMail": "Outlook Emails",
                 (etc.)
            }
        }
    }
}

The problem is that the values that come back inside "fcRequest" varies based on the parameters I am sending.

The class structure is as follows so far

    public class GetSubModulesResponse : BaseResponse
    {
        [JsonProperty("fcResponse")]
        public SubModuleResponse Response { get; set; }
    }

    public class SubModuleResponse
    {
        [JsonProperty("responseData")]
        public SubModuleData Data { get; set; }
    }

    public class SubModuleData
    {
        [JsonProperty("fcRequest")]
        public SubModuleFIMRequest RequestFIM { get; set; }

        [JsonProperty("fcRequest")]
        public SubModuleFSRequest RequestFS { get; set; }
    }

And this is the basic call structure

GetSubModulesResponse subModuleResponse = new GetSubModulesResponse();
var response = SubmitAPICall();
subModuleResponse = JsonConvert.DeserializeObject<GetSubModulesResponse>(response);

Now I know I obviously can't have the same JsonProperty on both RequestFIM and RequestFS, but what I'm trying to do is somehow find a way to switch which one of those two properties I should use based on a variable.


Solution

  • One option is to go with a custom (de-)serializer for the element. This way, you can still least benefit from automatic deserialization in most spots and get the flexibility where you need it. I'm assuming you're using Newtonsoft JSON / JSON.NET.

    Let's introduce a base class for the fcRequest elements first.

    public enum ResponseType
    {
        FIM, FS
    }
    
    public abstract class ResponseBase
    {
        [JsonIgnore]
        public abstract ResponseType ResponseType { get; }
    }
    

    By adding a ResponseType here you can simplify consuming code; if you can use type based pattern matching, you may not even need it.

    I obviously have no idea what your domain entities are, but for the sake of the argument, the SubModuleFIMRequest is now going to contain the mail addresses. In addition, it also derives from said ResponseBase:

    public class SubModuleFIMRequest : ResponseBase
    {
        public override ResponseType ResponseType => ResponseType.FIM;
    
        [JsonProperty("mail")]
        public string Mail { get; set; }
    
        [JsonProperty("outlookMail")]
        public string OutlookMail { get; set; }
    }
    

    Next, you'd implement a JsonConverter<ResponseBase>; to make life easy, it can deserialize the responseData content into a JObject first. In doing so, you'll be able to introspect the properties of the element, which in turn (hopefully) allows you to come up with a heuristic to determine the element's actual type.

    Once you know the type, you convert the JObject to a concrete instance. Here's an example:

    public class ResponseDataConverter : JsonConverter<ResponseBase>
    {
        /// <inheritdoc />
        public override bool CanWrite => false;
    
        /// <inheritdoc />
        public override ResponseBase ReadJson(JsonReader reader, Type objectType, ResponseBase existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var jObject = serializer.Deserialize<JObject>(reader);
    
            // Now, decide tye type by matching patterns.
            if (jObject.TryGetValue("mail", out var mailToken))
            {
                return jObject.ToObject<SubModuleFIMRequest>();
            }
    
            // TODO: Add more types as needed
    
            // If nothing matches, you may choose to throw an exception,
            // return a catchall type (e.g. wrapping the JObject), or just
            // return a default value as a last resort.
            throw new JsonSerializationException();
        }
    
        /// <inheritdoc />
        public override void WriteJson(JsonWriter writer, ResponseBase value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    

    Note that the serializer doesn't need to write, so we're just throwing in WriteJson.

    What's left is to annotate the SubModuleData's fcProperty property with a JsonConverter attribute pointing to the converter type:

    public class SubModuleData
    {
        [JsonProperty("fcRequest")]
        [JsonConverter(typeof(ResponseDataConverter))]
        public ResponseBase FcRequest { get; set; }
    }
    

    I hope that gets you started. As was mentioned in other comments and answers: If you can influence the API returning the JSON in the first place, try changing that instead.