0

Having this strange-looking json response:

{
    "invoices":{
       "0":{
          "invoice":{
             "id":"420",
             "invoicecontents":{
                "0":{
                   "invoicecontent":{
                      "name":"Here's the name of the content 0"
                   }
                },
                "1":{
                   "invoicecontent":{
                      "name":"Here's the name of the content 1"
                   }
                }
             }
          }
       },
       "1":{
         "invoice":{
            "id":"420",
            "invoicecontents":{
               "0":{
                  "invoicecontent":{
                     "name":"Here's the name of the content 0"
                  }
               }
            }
         }
      },
       "parameters":{
          "limit":"3",
          "page":"1",
          "total":"420"
       }
    },
    "status":{
       "code":"OK"
    }
 }

How do I change the structure into this easy-to-deserialize one?

{
   "invoices":[
      {
         "id":"420",
         "invoicecontents":[
            {
               "name":"Here's the name of the content 0"
            },
            {
               "name":"Here's the name of the content 1"
            }
         ]
      },
      {
         "id":"420",
         "invoicecontents":[
            {
               "name":"Here's the name of the content 0"
            }
         ]
      }
   ]
}

I'd like to deserialize into List of Invoices as below

class Invoice {
    public string Id { get; set; }
    
    [JsonProperty("invoicecontents")]
    public InvoiceContent[] Contents { get; set; }

    class InvoiceContent {
        public string Name { get; set; }
    }
}

There's no problem with getting the status code or parameters, I simply do this:

var parsed = JObject.Parse(jsonInvoices);

var statusCode = parsed?["status"]?["code"]?.ToString();
var parameters = parsed?["invoices"]?["parameters"]?;

The real problem starts when I'm trying to achieve easy-to-deserialize json structure I've mentioned before. I've tried something like this:

var testInvoices = parsed?["invoices"]?
    .SkipLast(1)
    .Select(x => x.First?["invoice"]);

But I can't manage to "repair" invoicecontents/invoicecontent parts. My goal is to deserialize and store the data.

Michał Droździk
  • 324
  • 1
  • 3
  • 10
  • 1
    so you want to change the json string from the hard version using string manipulation into the easy version before deserialization, correct? – draz Feb 01 '22 at 14:54
  • I would take the JSON as is and deserialize the property names that appear strange to you into a dictionary. E.g. the object value of `"invoices"` can be deserialized into a `Dictionary` if `Invoice.Contents` has the type `Dictionary`. The properties `"0"`, `"1"`, etc. will be the keys of those dictionaries. If you really, really want your model to look like your proposal then I'd make a second model hierarchy and map to it via C# code. – Good Night Nerd Pride Feb 01 '22 at 15:03
  • But your are right that in this particular case the JSON is weird. The index properties `"0"` etc. add no additional information that couldn't have been transmitted by using a simple JSON array. – Good Night Nerd Pride Feb 01 '22 at 15:23
  • You could design a data model for your desired JSON, and for each list that is serialized as a dictionary, use `ListToDictionaryConverter` from [this answer](https://stackoverflow.com/a/41559688/3744182) to [Display JSON object array in datagridview](https://stackoverflow.com/q/41553379/3744182). – dbc Feb 01 '22 at 15:25
  • @dbc I'll check that out and see the performance differences, but for now I'm happy it's working :) I agree that dictionary sounds more appropriate tho – Michał Droździk Feb 01 '22 at 17:16

2 Answers2

2

This isn't JSON.

JavaScript Object Notation literally describes Objects. What you have here is the punchline of a joke that starts out with: "A SQL JOIN, a StringBuilder, and a couple for loops walk into a bar..."

As others have demonstrated, JObject is great for working with JSON that would be impractical to define as classes. You can use Linq to navigate through it, but JSONPath Expressions can be much simpler.

Example 1: Let's get the status code.

var status = parsed.SelectToken(".status.code").Value<string>();

Example 2: Let's 'deserialize' our invoices.


public string Invoice 
{ 
  public string Id { get; set; } 
  public List<string> Content { get; set; }
}

public List<Invoice> GetInvoices(string badJson)
{
  var invoices = JObject.Parse(badJson).SelectTokens(".invoices.*.invoice");
  var results = new List<Invoice>();
  foreach (var invoice in invoices)
  {
    results.Add(new Invoice()
    {
      Id = invoice.Value<string>("id"),
      Contents = invoice.SelectTokens(".invoicecontents.*.invoicecontent.name")
        .Values<string>().ToList()
      // Note: JToken.Value<T> & .Values<T>() return nullable types
    });
  }
  return results;
}
Acktually
  • 76
  • 5
  • 2
    SelectTokens() is a cool idea, thanks! Well, as weird and incorrect as it looks, it is still a valid JSON file - and sadly I have to work with it. – Michał Droździk Feb 01 '22 at 18:56
1

try this

     var jsonParsed = JObject.Parse(json);
    
    var invoices = ((JObject) jsonParsed["invoices"]).Properties()
    .Select(x => (JObject) x.Value["invoice"] ).Where(x => x != null)
    .Select(s => new Invoice
    {
        Id = s.Properties().First(x => x.Name == "id").Value.ToString(),
        Contents = ((JObject)s.Properties().First(x => x.Name == "invoicecontents").Value).Properties()
        .Select(x => (string) x.Value["invoicecontent"]["name"]).ToList()
    }).ToList();

and I simplified your class too, I don't see any sense to keep array of classes with one string, I think it should be just array of strings

public class Invoice
{
    public string Id { get; set; }

    public List<string> Contents { get; set; }
}

result

[
  {
    "Id": "420",
    "Contents": [
      "Here's the name of the content 0",
      "Here's the name of the content 1"
    ]
  },
  {
    "Id": "420",
    "Contents": [
      "Here's the name of the content 0"
    ]
  }
]
Serge
  • 40,935
  • 4
  • 18
  • 45
  • Thanks a lot, that works! Of course I do have more properties in the invoice class that I'll need to fill up from incoming json, but I think it's more than a good way to do it as you suggested :) – Michał Droździk Feb 01 '22 at 17:12