Manipulating a Json string from within a custom Json Converter
Asked Answered
S

1

0

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
Sorcim answered 28/4, 2021 at 11:57 Comment(5)
I'm not certain, but it looks to me like you are ultimately trying to flatten some deeply nested JSON down to a simpler structure. You've found a converter I wrote that sort of does that, but not to the extent you want, so your solution is to try to manipulate the JSON with Regexes to make it fit the converter (which is not a good approach, IMO). I think what you might be looking for is Can I specify a path in an attribute to map a property in my class to a child property in my JSON?. If I've misunderstood, please clarify.Jermainejerman
You understood correctly @BrianRogers, thanks. I agree using Regex is messy and I am indeed looking for an alternative approach. From your link, do you have to decorate every single Json property with "[JsonProperty("...")]"? Is adding all these hardcoded names not a maintenance liability? Is it not better to use a custom SerializationBinder so that all the Hardcoded names are centralised there? If not, why not?Sorcim
@BrianRogers, also Activator.CreateInstance(objectType) fails because my object has no parameterless constructor.Sorcim
With the JsonPathConverter in the other answer you would only need to decorate those properties that either have a different name than what is in the JSON or which need to be mapped to a child object. But based on your constraints, this converter isn't going to work for you either. It won't handle non-default constructors (as you've seen), and it doesn't have a way to translate true to false and vice versa for the IsPublic property example. You are going to need a custom converter specifically for your situation.Jermainejerman
A SerializationBinder doesn't map JSON properties to class properties, it maps type names and assembly names in the JSON (in a special $type meta property) to actual types. So that's not going to help you with flattening.Jermainejerman
J
1

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

Jermainejerman answered 30/4, 2021 at 7:4 Comment(6)
Thanks a lot Brian, that is very helpful. It looks like I am condemned to have to create a bit of a mess either way then. In this example, it does look tidy, but my real Json is huge. There are many things to modify and many objects I would need to instantiate. Also, using "SelectToken" means that I will have to figure out the "path" to each of these properties in the Json, which might get tricky to get right.Sorcim
Why are you advising against the approach I suggested, that modifies the string globally in one place using Regex + returning the object from ReadJson() using "return JsonConvert.DeserializeObject<RootObject>(modifiedJsonString );"? What are the pitfalls?Sorcim
Well, for one thing, you have Json.Net available, a tool which was built to parse and manipulate JSON. Why would you choose error-prone regex string manipulation instead of using the parser? It doesn't seem very "clean" IMO. Secondly, you said that you have objects which lack default constructors. So that means the parameterized constructors must be used. I don't know what those parameters actually are, but if they are more complicated than simple primitives or involve circular references, then you can run into issues. (cont'd)Jermainejerman
An approach which extracts the parameters first and then calls the constructor seemed more likely to succeed, from my vantage point. But ultimately it is your code, and you know more about your needs than I do, so you can build it however you like. If regex works better for you, then go for it.Jermainejerman
I see that your approach may be best in 99% of cases, but I might want to stick to Regex in this case. It is good to consider both approaches though. I very much appreciate your tips and inputs.Sorcim
Cheers, glad I could help!Jermainejerman

© 2022 - 2024 — McMap. All rights reserved.