Search code examples
c#jsonparsingjson.netjsonconvert

Manipulating a Json string from within a custom Json Converter


In order to ask my question, I will be referring to @Brian Rogers 's answer here. In this answer ReadJson is doing something relatively simple. How could I however add a layer in there in order to manipulate the incoming Json string before deserialising it into an object and then returning it?

Here is the type of things I would like to do (modified version of Brian's WrappedObjectConvert class):

class WrappedObjectConverter : JsonConverter
{
    private string CustomParsing(string jsonString)
    {
         string modifiedJsonString;

         // Some renaming
         modifiedJsonString= Regex.Replace(modifiedJsonString, $@"(?<="")CarName(?="":\s)", "Myname", RegexOptions.IgnoreCase);

         modifiedJsonString= Regex.Replace(modifiedJsonString, $@"(?<="")CustName(?="":\s)", "Myname", RegexOptions.IgnoreCase);

         modifiedJsonString= Regex.Replace(modifiedJsonString, $@"(?<="")MyName(?="":\s)", "Myname", RegexOptions.IgnoreCase);

         modifiedJsonString= Regex.Replace(modifiedJsonString, $@"(?<="")SomeAddr(?="":\s)", "AddressLine1 ", RegexOptions.IgnoreCase);

         // Renaming IsPublic true/false to IsPrivate false/true
        modifiedJsonString= Regex.Replace(modifiedJsonString, "\"IsPublic\": true,", "\"IsPrivate\": false,", RegexOptions.IgnoreCase);
        modifiedJsonString = Regex.Replace(modifiedJsonString, "\"IsPublic\": false,", "\"IsPrivate\": true,", RegexOptions.IgnoreCase);
    }

    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);

        string modifiedJsonString = CustomParsing(token.ToString());

        return ????;  // How to return the object

        // I could do something of the sort, but not sure it's got its place here:  
        // return JsonConvert.DeserializeObject<RootObject>(modifiedJsonString );  
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

The client class has also been slightly modified by adding the field "IsPrivate":

public class Client
{
    [JsonConverter(typeof(WrappedObjectConverter))]
    public List<Product> ProductList { get; set; }

    [JsonConverter(typeof(WrappedObjectConverter))]
    public string Name { get; set; }

    [JsonConverter(typeof(WrappedObjectConverter))]
    public bool IsPrivate { get; set; }

    [JsonConverter(typeof(WrappedObjectConverter))]
    public string AddressLine1 { get; set; }
}

And the demo with a modified Json (some labels have been changed from Brian's example, which need to be parsed and modified):

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""Result"": {
                ""Client"": {
                    ""ProductList"": {
                        ""Product"": [
                            {
                                ""MyName"": {
                                    ""CarName"": ""Car polish"",
                                    ""IsPublic"": ""True""
                                }
                            }
                        ]
                    },
                    ""MyName"": {
                        ""CustName"": ""Mr. Clouseau""
                    },
                    ""AddressLine1"": {
                        ""SomeAddr"": ""Hightstreet 13""
                    }
                }
            }
        }";

        RootObject obj = JsonConvert.DeserializeObject<RootObject>(json);

        Client client = obj.Result.Client;
        foreach (Product product in client.ProductList)
        {
            Console.WriteLine(product.Name);
        }
        Console.WriteLine(client.Name);
        Console.WriteLine(client.AddressLine1);
    }
}

As you can see, the way the parsing is being done is a bit hacky, so my questions are:

  1. Can I incorporate this parsing to the classes themselves without making a mess of my classes?
  2. If my approach is the way to go, how do I recreate the object so that ReadJson() can return it (see question marks in code above)
  3. Taking it to the next level: If the client class had a constructor taking in arguments (passed to a base class), how would you do 2. (as the extra level of nesting would complicate things I believe)
  4. If this is the wrong way to go, I am open to suggestions

Solution

  • From your question and comments it sounds like you have some complex JSON and your goal is to flatten it down into a simpler class structure. But you have some additional constraints:

    • The target classes for deserialization don't have default (parameterless) constructors.
    • You want to map the keys in the JSON to differently-named properties in the classes.
    • For some properties you want to translate the values as well (e.g. convert true to false and vice versa).
    • You want to keep the mapping logic all in one place, separate from the classes.

    You can do all this with a custom JsonConverter. The key is to load the JSON data into a JObject inside the converter. From there you can use SelectToken to specify paths to retrieve specific pieces of data from the JObject. You can then use these pieces to construct your objects via their non-default constructors. At the same time you can translate any values that require it.

    For example, let's say you are starting with the JSON in your question, and the classes you really want to deserialize to look like this:

    public class Client
    {
        public Client(string name, string addressLine1, List<Product> productList)
        {
            Name = name;
            AddressLine1 = addressLine1;
            ProductList = productList;
        }
    
        public List<Product> ProductList { get; set; }
        public string Name { get; set; }
        public string AddressLine1 { get; set; }
    }
    
    public class Product
    {
        public Product(string name, bool isPrivate)
        {
            Name = name;
            IsPrivate = isPrivate;
        }
    
        public string Name { get; set; }
        public bool IsPrivate { get; set; }
    }
    

    Here is a custom converter that will handle the deserialization:

    class CustomConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Client);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject obj = JObject.Load(reader);
    
            string name = (string)obj.SelectToken("Result.Client.MyName.CustName");
            string addressLine1 = (string)obj.SelectToken("Result.Client.AddressLine1.SomeAddr");
            List<Product> productList = obj.SelectToken("Result.Client.ProductList.Product")
                .Select(jt =>
                {
                    string prodName = (string)jt.SelectToken("MyName.CarName");
                    bool isPublic = string.Equals((string)jt.SelectToken("MyName.IsPublic"), "True", StringComparison.OrdinalIgnoreCase);
                    return new Product(prodName, !isPublic);
                })
                .ToList();
    
            Client client = new Client(name, addressLine1, productList);
            return client;
        }
    
        public override bool CanWrite => false;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    To use the converter, you can either add a [JsonConverter] attribute to the Client class like this:

    [JsonConverter(typeof(CustomConverter))]
    public class Client
    {
       ...
    }
    

    Or you can pass the converter as a parameter to JsonConvert.DeserializeObject() like this:

    Client client = JsonConvert.DeserializeObject<Client>(json, new CustomConverter());
    

    Here is a working demo: https://dotnetfiddle.net/EwtQHh