简体   繁体   中英

flattening json structure with arrays to multiple flat objects without arrays

I'm not sure whether did I describe the problem in subject 100% correctly, but I believe that the examples will do the trick.

I have JSON structure like below (note: there is small chance that this might change, so i need to lean forward to generic solution)

One invoice with multiple line items :

{
    "contactName": "Company",
    "lineItems": [
     {
        "quantity": 7.0,
        "description": "Beer No* 45.5 DIN KEG"
     },
     {
        "quantity": 2.0,
        "description": "Beer Old 49.5 DIN KEG"
     }
     ],
    "invoiceNumber": "C6188372"
}

And this is the wanted result data structure ( multiple invoices with duplicated data and different line item info ):

[{
    "contactName": "Company",
    "quantity": 7.0,
    "description": "Beer No* 45.5 DIN KEG"
    "invoiceNumber": "C6188372"
},{
    "contactName": "Company",
    "quantity": 2.0,
    "description": "Beer Old 49.5 DIN KEG"
    "invoiceNumber": "C6188372"
}]

So each "line item" from "invoice" should "result" in new invoice with duplicated other elements.

Small variations around result data structure are accepted, i can adjust my code around it. I've been spinning around using several similar questions such as:

For more background, i need this for CSV export. So result set should be two rows in generated CSV.

Any hints/tips are much appreciated. Thanks.

You could do it with a function like this:

//Pass in the name of the array property you want to flatten
public string FlattenJson(string input, string arrayProperty)
{
    //Convert it to a JObject
    var unflattened = JsonConvert.DeserializeObject<JObject>(input);

    //Return a new array of items made up of the inner properties
    //of the array and the outer properties
    var flattened = ((JArray)unflattened[arrayProperty])
        .Select(item => new JObject(
            unflattened.Properties().Where(p => p.Name != arrayProperty), 
            ((JObject)item).Properties()));

    //Convert it back to Json
    return JsonConvert.SerializeObject(flattened);
}

And call it like this:

var flattenedJson = FlattenJson(inputJson, "lineItems");

With external lib Cinchoo ETL - an open source library, you can convert your JSON to expected CSV format with few lines of code

string json = @"{
    ""contactName"": ""Company"",
    ""lineItems"": [
     {
        ""quantity"": 7.0,
        ""description"": ""Beer No* 45.5 DIN KEG""
     },
     {
        ""quantity"": 2.0,
        ""description"": ""Beer Old 49.5 DIN KEG""
     }
     ],
    ""invoiceNumber"": ""C6188372""
}";

StringBuilder sb = new StringBuilder();
using (var p = ChoJSONReader.LoadText(json))
{
    using (var w = new ChoCSVWriter(sb)
        .WithFirstLineHeader()
        )
        w.Write(p
            .SelectMany(r1 => ((dynamic[])r1.lineItems).Select(r2 => new
            {
                r1.contactName,
                r2.quantity,
                r2.description,
                r1.invoiceNumber
            })));
}
Console.WriteLine(sb.ToString());

Output CSV:

contactName,quantity,description,invoiceNumber
Company,7,Beer No* 45.5 DIN KEG,C6188372
Company,2,Beer Old 49.5 DIN KEG,C6188372

Hope it helps.

You could use a custom JsonConverter if you are able to Deserialize/Serialize into a strongly typed class. Invoice information I would imagine should be in some semi-structured object so this should be doable:

public class Invoice
{
    public string ContactName { get; set; }
    public List<Item> LineItems { get; set; } = new List<Item>();
    public string InvoiceNumber { get; set; }
}

public class Item
{
    public double Quantity { get; set; }
    public string Description { get; set; }
}

And then with the JsonConverter you can flatten it based upon the Items (Or any other property/properties you may want)

public class InvoiceFlattener : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var obj = value as Invoice;
        if (obj == null)
        {
            return;
        }

        writer.WriteStartArray();

        foreach (var item in obj.LineItems)
        {
            writer.WriteStartObject();
            writer.WritePropertyName(nameof(obj.ContactName));
            writer.WriteValue(obj.ContactName);
            writer.WritePropertyName(nameof(item.Quantity));
            writer.WriteValue(item.Quantity);
            writer.WritePropertyName(nameof(item.Description));
            writer.WriteValue(item.Description);
            writer.WritePropertyName(nameof(obj.InvoiceNumber));
            writer.WriteValue(obj.InvoiceNumber);
            writer.WriteEndObject();
        }

        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Invoice);
    }
}

And to use this Converter you supply it when Serializing

        var invoice = JsonConvert.DeserializeObject<Invoice>(inputJson);
        var outputJson = JsonConvert.SerializeObject(invoice, new InvoiceFlattener());

As you have probably worked out this converter doesn't work when deserializing but if this is a requirement you can write the logic in the ReadJson converter method. The downside to this is you will be required to maintain the converter should the structure of the Invoice class ever change. But it keeps us in a strongly typed world

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM