0

I have a JSON string (simplified for the example):

[
    {
        "name": "name 1",
        "profiles":[{
            "name": "profile 1.1"
            },{
            "name": "profile 1.2"
        }]
    },{
        "name": "name 2",
        "profiles":[{
            "name": "profile 2.1"
            }]
    }
]

and I would like to change the name of the property profiles.name to profileName, knowing that this JSON is dynamic so I don't have a related .Net class.

Note that the JSON is big; parsing all the its objects by deserializing them into a combination of arrays and Dictionary instances is working but is not efficient.

dbc
  • 104,963
  • 20
  • 228
  • 340
Eric
  • 230
  • 1
  • 2
  • 15
  • Most JSON serializers/deserializers let you deserialize to a combination of arrays and `Dictionary` instances. Have you tried doing something with that? – T.J. Crowder Nov 19 '19 at 10:22
  • Yes, it is working but not efficient when the json is big as we have to parse all the objects – Eric Nov 19 '19 at 10:24
  • 1
    So is your question, *How can I rename the properties `..profiles[*].name` to `..profiles[*].profileName`using some sort of streaming transformation, without loading the entire JSON into memory?* Because you can do it easily with [tag:json.net] if you load the entire thing into memory, see https://dotnetfiddle.net/W46243 – dbc Nov 19 '19 at 20:16

1 Answers1

2

You want to rename all properties named "name" to "profileName" when nested inside some object ..profiles[*] in free-form JSON whose schema is not known in advance and that may be very large.

This can be accomplished by installing .

Firstly, if you can load the entire JSON into memory, this can be done very easily using LINQ to JSON which has functionality to parse, modify, and re-serialize arbitrary JSON with any schema. First, grab the extension method public static void Rename(this JToken token, string newName) from this answer to Rename JProperty in json.net by Brian Rogers. Now you can rename your properties as follows:

var root = JToken.Parse(jsonString);

foreach (var item in root.SelectTokens("..profiles[*].name").ToList())
    item.Rename("profileName"); // Using the extension method here

var newJsonString = root.ToString();

Note the use of the method SelectTokens(). This method provides support for JSONPath - XPath for JSON query syntax to select nested JSON tokens out of the JSON hierarchy matching some pattern. If your actual JSON differs from the JSON in the question you would need to tweak the JSONPath accordingly.

Demo fiddle #1 here.

Secondly, if you cannot load the entire JSON file into memory because of its size, you can implement a streaming transformation from file to file by subclassing JsonTextWriter and overriding WritePropertyName().

Define the following class and extension methods:

public class NameRemappingJsonWriter : JsonTextWriter
{
    readonly Func<string, int, string, string> map;

    public NameRemappingJsonWriter(TextWriter textWriter, Func<string, int, string, string> map) : base(textWriter)
    {
        if (map == null)
            throw new ArgumentNullException(nameof(map));
        this.map = map;
    }

    public override void WritePropertyName(string name)
    {
        base.WritePropertyName(map(name, Top, Path));
    }       

    public override void WritePropertyName(string name, bool escape)
    {
        base.WritePropertyName(map(name, Top, Path), escape);
    }       
}

public static partial class JsonExtensions
{
    public static void StreamRenameJsonProperties(string inFilePath, string outFilePath, string oldName, Regex parentPathRegex, string newName, Formatting formatting = Formatting.Indented)
    {
        Func<string, int, string, string> map = (name, depth, parentPath) =>
        {
            if (name == oldName && parentPathRegex.IsMatch(parentPath))
                return newName;
            return name;
        };
        StreamRenameJsonProperties(inFilePath, outFilePath, map, formatting);
    }

    public static void StreamRenameJsonProperties(string inFilePath, string outFilePath, Func<string, int, string, string> map, Formatting formatting = Formatting.Indented)
    {
        using (var textReader = new StreamReader(inFilePath))
        using (var jsonReader = new JsonTextReader(textReader) { DateParseHandling = DateParseHandling.None })
        {
            using (var textWriter = new StreamWriter(outFilePath))
            using (var jsonWriter = new NameRemappingJsonWriter(textWriter, map) { Formatting = formatting })
            {
                jsonWriter.WriteToken(jsonReader);
            }
        }
    }
}

And you will be able to transform your JSON file as follows:

JsonExtensions.StreamRenameJsonProperties(inFilePath, outFilePath, "name", new Regex("profiles\\[[0-9]+\\]$"), "profileName");

If your actual JSON differs from the JSON in the question you would need to tweak the oldName and Regex accordingly.

Demo fiddle #2 here. In both fiddles the output is

[
  {
    "name": "name 1",
    "profiles": [
      {
        "profileName": "profile 1.1"
      },
      {
        "profileName": "profile 1.2"
      }
    ]
  },
  {
    "name": "name 2",
    "profiles": [
      {
        "profileName": "profile 2.1"
      }
    ]
  }
]
dbc
  • 104,963
  • 20
  • 228
  • 340
  • Seriously, I have rarely seen an answer this clear and detailled ! Thank you very much, this will clearly help me ! – Eric Nov 20 '19 at 08:39