0

I have to send a variables Json to Mailgun but it only accepts the curly braces format when using multi level json files. So,

How can I pass from this:

{ "vehicle.type": "car"}

To this, with C#

{"vehicle": {"type": "car"}}

Having into consideration that sometimes it could be up to 3 nested elements. Like element1.element2.element3: value

mrbitzilla
  • 368
  • 3
  • 11
  • Do you statically know the property names or does this need to work for arbitrary dotted keys? – Aluan Haddad May 16 '20 at 01:52
  • Property names may vary. The only indicator are the dots which tell who is a parent and who is a child (or both) – mrbitzilla May 16 '20 at 02:01
  • A reliable way to do this would be to deserialize the object, construct an expanded version recursively based on the naming convention described, and then serialize that object. This may seem like a lot of work but if there are multiple properties in the source JSON it is likely worth it. – Aluan Haddad May 16 '20 at 02:07
  • What do you mean by "construct an expanded version" ? – mrbitzilla May 16 '20 at 02:13
  • 1
    Something like `IDictionaryExpand(IDictionary d) { var result = new Dictionary(); foreach(var (key, value) in d) {var segments = key.Split('.'); d[segments[0]] = segments.Length > 1 ? d[segments[0]] = Expand(new Dictionary{ [string.Join(".", keys.Skip(1))] = value}) : value; } return result; }` – Aluan Haddad May 16 '20 at 02:25
  • In the input JSON, are all the values strings? – Brian Rogers May 16 '20 at 02:32
  • Yes, all strings – mrbitzilla May 16 '20 at 02:38
  • 1
    There's [this](https://stackoverflow.com/a/2391008/1202807) (use a custom converter to deserialize, then serialize again normally), or [this](https://stackoverflow.com/a/22985802/1202807). – Gabriel Luci May 16 '20 at 02:48
  • Thanks to everyone for their help. Just would like to add this nuget package I found which also does the job. https://www.nuget.org/packages/JsonFlatten/ – mrbitzilla May 24 '20 at 17:58

2 Answers2

1

Here is tricky way using string replacement. replace for any dot(.) with this (": {") and add close tag (}) at the end for every dot(.) count. Good luck!

Try This:

IDictionary<string, object> Expand(IDictionary<string, object> d)
{
    var result = new Dictionary<string, object>();
    foreach (KeyValuePair<string, object> item in d)
    {
        var segments = item.Key.Split('.');
        if (segments.Length > 1)
        {
            if (result.ContainsKey(segments[0]))
            {
                dynamic obj = new ExpandoObject();
                obj = result[segments[0]];
                IDictionary<string, object> myObj = obj;
                myObj.Add(string.Join(".", segments.Skip(1)), item.Value);

                result[segments[0]] = Expand(myObj);
            }
            else
            {
                result[segments[0]] = Expand(new Dictionary<string, object>
                {
                    [string.Join(".", segments.Skip(1))] = item.Value
                });
            }
        }
        else
        {
            result[segments[0]] = item.Value;
        }
    }

    return result;
}
  • Already tried but it doesn't work for this case. I could have several of these nodes since it's a JSON text file. The example above is just one node of the many you could have. – mrbitzilla May 16 '20 at 02:03
  • send me sample complex data that you may have – pujangga malam May 16 '20 at 02:55
  • Current: {"hospital.Name": "BestOneEver", "hospital.Estatus": "Active", "hospital.hospitalExtraData1.Street": "43", "hospital.hospitalExtraData1.Color": "Blue", "hospital.hospitalExtraData1.hospitalExtraData2.IsExpensive": "No", "hospital.hospitalExtraData1.hospitalExtraData2.Works24Hrs": "Yes", "patient.Name": "Leonel Messi", "patient.Age": "23"} – mrbitzilla May 16 '20 at 03:05
  • Expected: {"hospital":{"Name":"BestOneEver","Estatus":"Active","hospitalExtraData1":{"Street":"43","Color":"Blue","hospitalExtraData2":{"IsExpensive":"No","Works24Hrs":"Yes"}}},"patient":{"Name":"Leonel Messi","Age":"23"}} – mrbitzilla May 16 '20 at 03:05
  • 1
    @RolandoMedina I've updated my previous answer with sample code. Please validate. – pujangga malam May 16 '20 at 04:51
1

Here is what I recommend.

note: I am using the Newtonsoft.Json library available via Nuget, if you are using .NET Core, you can use the built in System.Text.Json library.

Because we have multiple properties in the object with flattened property keys, qualified with .s and we need to convert these properties into a hierarchical, nested JSON structure, merging siblings appropriately at each level, a simple string replacement is neither safe nor effective.

Therefore, the approach here will be to parse the flattened property keys, such as "hospital.hospitalExtraData1.Street" recursively inferring and creating a hierarchy of nested objects.

Let's begin

var originalJson = @"{
  ""hospital.Name"": ""BestOneEver"",
  ""hospital.Estatus"": ""Active"",
  ""hospital.hospitalExtraData1.Street"": ""43"",
  ""hospital.hospitalExtraData1.Color"": ""Blue"",
  ""hospital.hospitalExtraData1.hospitalExtraData2.IsExpensive"": ""No"",
  ""hospital.hospitalExtraData1.hospitalExtraData2.Works24Hrs"": ""Yes"",
  ""patient.Name"": ""Leonel Messi"",
  ""patient.Age"": ""23""
}";

var original = JsonConvert.DeserializeObject<IDictionary<string, object>>(originalJson);

Now we have an object model we can work with and restructure.

We will do this using recursion

var original = JsonConvert.DeserializeObject<IDictionary<string, object>>(originalJson);

IDictionary<string, object> Expand(IDictionary<string, object> input)
{
    var result = new Dictionary<string, object>();
    foreach (var property in input)
    {
        var (key, remainder) = ParseKey(property.Key);
        if (!result.ContainsKey(key))
        {
            result[key] = remainder != null 
                ? Expand(new Dictionary<string, object>
                {
                    [remainder] = property.Value
                })
                : property.Value;
        }
        else if (result[key] is IDictionary<string, object> inner)
        {
            inner[remainder] = property.Value;
            result[key] = Expand(inner);
        }
        else
        {
            result[key] = property.Value;
        }
    }
    return result;
}
(string key, string remainder) ParseKey(string key)
{
    var dotIndex = key.IndexOf('.');
    if (dotIndex != -1)
    {
        return (key.Substring(0, dotIndex), key.Substring(dotIndex + 1));
    }
    return (key, null);
}

var expanded = Expand(original);

var expandedJson = JsonConvert.SerializeObject(expanded, Newtonsoft.Json.Formatting.Indented);

Result:

{
  "hospital": {
    "Name": "BestOneEver",
    "Estatus": "Active",
    "hospitalExtraData1": {
      "Street": "43",
      "Color": "Blue",
      "hospitalExtraData2": {
        "IsExpensive": "No",
        "Works24Hrs": "Yes"
      }
    }
  },
  "patient": {
    "Name": "Leonel Messi",
    "Age": "23"
  }
}
Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • Glad I could help. There are a few limitation to this solutions that I'm not sure how to work around. Specifically, if there is a were an integral subkey in source data, for example, `"Hospital.EStatuses.0.Name"` it _could_ very well imply that the expanded form should have an array as `"EStatuses"` but, it is ambiguous because it _could_ be an object. For simplicity I went with an object for all nested structures. – Aluan Haddad May 16 '20 at 05:40
  • I see. Well, thankfully my data wouldn’t go under that scenario for now since my entities all are named. – mrbitzilla May 17 '20 at 06:11
  • 1
    @RolandoMedina it would be easy to add expansion into arrays if that becomes necessary, the problem is that it is ambiguous because `"0"` is a perfectly valid property object property name. You would have to decide how to handle `"x.0"` and the decision is fairly arbitrary. – Aluan Haddad May 17 '20 at 06:41