Modifying a JSON File Using System.Text.JSON

Modifying a JSON file using System.Text.Json

Your problem is that you would like to retrieve, filter, and pass along some JSON without needing to define a complete data model for that JSON. With Json.NET, you could use LINQ to JSON for this purpose. Your question is, can this currently be solved as easily with System.Text.Json?

As of .NET 6, this cannot be done quite as easily with System.Text.Json because it has no support for JSONPath which is often quite convenient in such applications. There is currently an open issue Add JsonPath support to JsonDocument/JsonElement #41537 tracking this.

That being said, imagine you have the following JSON:

[
{
"id": 1,
"name": "name 1",
"address": {
"Line1": "line 1",
"Line2": "line 2"
},
// More properties omitted
}
//, Other array entries omitted
]

And some Predicate<long> shouldSkip filter method indicating whether an entry with a specific id should not be returned, corresponding to CHECKS in your question. What are your options?

In .NET 6 and later you could parse your JSON to a JsonNode, edit its contents, and return the modified JSON. A JsonNode represents an editable JSON Document Object Model and thus most closely corresponds to Newtonsoft's JToken hierarchy.

The following code shows an example of this:

var root = JsonNode.Parse(rawJsonDownload).AsArray(); // AsArray() throws if the root node is not an array.
for (int i = root.Count - 1; i >= 0; i--)
{
if (shouldSkip(root[i].AsObject()["id"].GetValue<long>()))
root.RemoveAt(i);
}

return Json(root);

Mockup fiddle #1 here

In .NET Core 3.x and later, you could parse to a JsonDocument and return some filtered set of JsonElement nodes. This works well if the filtering logic is very simple and you don't need to modify the JSON in any other way. But do note the following limitations of JsonDocument:

  • JsonDocument and JsonElement are read-only. They can be used only to examine JSON values, not to modify or create JSON values.

  • JsonDocument is disposable, and in fact must needs be disposed to minimize the impact of the garbage collector (GC) in high-usage scenarios, according to the docs. In order to return a JsonElement you must clone it.

The filtering scenario in the question is simple enough that the following code can be used:

using var usersDocument = JsonDocument.Parse(rawJsonDownload);
var users = usersDocument.RootElement.EnumerateArray()
.Where(e => !shouldSkip(e.GetProperty("id").GetInt64()))
.Select(e => e.Clone())
.ToList();

return Json(users);

Mockup fiddle #2 here.

In any version, you could create a partial data model that deserializes only the properties you need for filtering, with the remaining JSON bound to a [JsonExtensionDataAttribute] property. This should allow you to implement the necessary filtering without needing to hardcode an entire data model.

To do this, define the following model:

public class UserObject
{
[JsonPropertyName("id")]
public long Id { get; set; }

[System.Text.Json.Serialization.JsonExtensionDataAttribute]
public IDictionary<string, object> ExtensionData { get; set; }
}

And deserialize and filter as follows:

var users = JsonSerializer.Deserialize<List<UserObject>>(rawJsonDownload);
users.RemoveAll(u => shouldSkip(u.Id));

return Json(users);

This approach ensures that properties relevant to filtering can be deserialized appropriately without needing to make any assumptions about the remainder of the JSON. While this isn't as quite easy as using LINQ to JSON, the total code complexity is bounded by the complexity of the filtering checks, not the complexity of the JSON. And in fact my opinion is that this approach is, in practice, a little easier to work with than the JsonDocument approach because it makes it somewhat easier to inject modifications to the JSON if required later.

Mockup fiddle #3 here.

No matter which you choose, you might consider ditching WebClient for HttpClient and using async deserialization. E.g.:

var httpClient = new HttpClient(); // Cache statically and reuse in production
var root = await httpClient.GetFromJsonAsync<JsonArray>("WEB API CALL");

Or

using var usersDocument = await JsonDocument.ParseAsync(await httpClient.GetStreamAsync("WEB API CALL"));

Or

var users = await JsonSerializer.DeserializeAsync<List<UserObject>>(await httpClient.GetStreamAsync("WEB API CALL"));

You would need to convert your API method to be async as well.

C# How to change a single value in a dynamic json object with System.Text.Json?

try this

    string json = File.ReadAllText(filepath);

var settings = JsonNode.Parse(json);
settings ["url"]= "new url";
json=settings.ToJsonString(new JsonSerializerOptions { WriteIndented = true });

File.WriteAllText(filepath, json);

How to delete and update based on a path in System.Text.Json (.NET 6)?

In short: Unfortunately you can't

Modification

JsonDocument is readonly by design. Before JsonNode has been introduced you had to

  1. deserialize it as Dictionary<string, object>
  2. perform the modification
  3. serialize it back to json

Since JsonNode has been introduced in .NET 6 you can do the following

var node = JsonNode.Parse(json);
var rootObject = node as JsonObject;
var aArray = rootObject["a"] as JsonArray;
var firstA = aArray[0];
firstA["a3"] = "modified";

or in short

var node = JsonNode.Parse(json);
node["a"][0]["a3"] = "modified";

Locate element by Path

Even though the need has been expressed in 2019 it hasn't been address yet. But as layomia said

This feature is proposed for .NET 6, but not committed. To be clear, we acknowledge that this is an important feature for many users, however, work on features with higher priority may prevent this from coming in .NET 6.

Unfortunately this feature did not make it to the .NET 6.

There are 3rd party libraries which offers this capability for JsonDocument. One of the most mature one is called JsonDocumentPath. With its SelectElement method you can really easily retrieve the given field as JsonElement

var doc = JsonDocument.Parse(json);
JsonElement? a3 = doc.RootElement.SelectElement("$.a[0].a3");

Interoperability

Even though there is a way to convert JsonElement to JsonNode and in the other way around:

var a3Node = JsonSerializer.Deserialize<JsonNode>(a3.Value);
a3Node = "a";

it does not really help since the a3Node represents only the a3 field and according to my understanding you can't just merge two JsonNodes.

how to change newtonsoft.json code to system.text.json

Please refer to the official How to migrate from Newtonsoft.Json to System.Text.Json.

There are 3.1 and 5 versions provided. Please note that in 3.1 you can install the 5.0 package to get the new features (for example deserializing fields).

Create JSON object using System.Text.Json

In .NET 5 you can use a combination of dictionaries and anonymous types to construct free-form JSON on the fly. For instance, your sample code can be rewritten for System.Text.Json as follows:

var json = new Dictionary<string, object>
{
["Status"] = result.Status.ToString(),
["Duration"] = result.TotalDuration.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo),
};

var entries = result.Entries.ToDictionary(
d => d.Key,
d => new { Status = d.Value.Status.ToString(), Duration = d.Value.Duration.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo), Description = d.Value.Description } );
if (entries.Count > 0)
json.Add("result", entries);

var newJson = JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true });

Notes:

  • When constructing a JSON object with properties whose values have mixed types (like the root json object in your example) use Dictionary<string, object> (or ExpandoObject, which implements Dictionary<string, object>).

    You must use object for the value type because System.Text.Json does not support polymorphism by default unless the declared type of the polymorphic value is object. For confirmation, see How to serialize properties of derived classes with System.Text.Json:

    You can get polymorphic serialization for lower-level objects if you define them as type object.

  • When constructing a JSON object with a fixed, known set of properties, use an anonymous type object.

  • When constructing a JSON object with runtime property names but fixed value types, use a typed dictionary. Oftentimes LINQ's ToDictionary() method will fit perfectly, as is shown above.

  • Similarly, when constructing a JSON array with values that have mixed types, use List<object> or object []. A typed List<T> or T [] may be used when all values have the same type.

  • When manually formatting numbers or dates as strings, be sure to do so in the invariant locale to prevent locale-specific differences in your JSON.

Demo fiddle here.

Is it possible to deserialize json string into dynamic object using System.Text.Json?

tl:dr JsonNode is the recommended way but dynamic typing with deserializing to ExpandoObject works and I am not sure why.

It is not possible to deserialize to dynamic in the way you want to. JsonSerializer.Deserialize<T>() casts the result of parsing to T. Casting something to dynamic is similar to casting to object

Type dynamic behaves like type object in most circumstances. In particular, any non-null expression can be converted to the dynamic type. The dynamic type differs from object in that operations that contain expressions of type dynamic are not resolved or type checked by the compiler. The compiler packages together information about the operation, and that information is later used to evaluate the operation at run time

docs.

The following code snippet shows this happening with your example.

var jsonString = "{\"foo\": \"bar\"}";
dynamic data = JsonSerializer.Deserialize<dynamic>(jsonString);
Console.WriteLine(data.GetType());

Outputs: System.Text.Json.JsonElement

The recommended approach is to use the new JsonNode which has easy methods for getting values. It works like this:

JsonNode data2 = JsonSerializer.Deserialize<JsonNode>(jsonString);
Console.WriteLine(data2["foo"].GetValue<string>());

And finally trying out this worked for me and gives you want you want but I am struggling to find documentation on why it works because according to this issue it should not be supported but this works for me. My System.Text.Json package is version 4.7.2

dynamic data = JsonSerializer.Deserialize<ExpandoObject>(jsonString);
Console.WriteLine(data.GetType());
Console.WriteLine(data.foo);

Change values in JSON file (writing files)

Here's a simple & cheap way to do it (assuming .NET 4.0 and up):

string json = File.ReadAllText("settings.json");
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
jsonObj["Bots"][0]["Password"] = "new password";
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText("settings.json", output);

The use of dynamic lets you index right into json objects and arrays very simply. However, you do lose out on compile-time checking. For quick-and-dirty it's really nice but for production code you'd probably want the fully fleshed-out classes as per @gitesh.tyagi's solution.



Related Topics



Leave a reply



Submit