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
andJsonElement
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 aJsonElement
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
- deserialize it as
Dictionary<string, object>
- perform the modification
- 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 JsonNode
s.
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) useDictionary<string, object>
(orExpandoObject
, which implementsDictionary<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 isobject
. 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>
orobject []
. A typedList<T>
orT []
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
How to Bring Up the Built-In File Copy Dialog
How to Get the Path of the Current User's "Application Data" Folder
C# Winforms Errorprovider Control
C# Creating an Unknown Generic Type at Runtime
Data Contract Serializer - How to Omit the Outer Element of a Collection
Datagrid Shows Path of Image Instead of Image Itself
Custom Authentication in ASP.NET-Core
Do the New C# 5.0 'Async' and 'Await' Keywords Use Multiple Cores
Connection Timeout for SQL Server
Cannot Find .Cs Files for Debugging .Net Source Code
How to Clear Tracked Entities in Entity Framework
How to Create Migrations After Upgrading to ASP.NET Core 2.0
How to Make the Xmlserializer Only Serialize Plain Xml
How to Get Some Values from a JSON String in C#
Get Current Url in a Blazor Component
Detect If PDF File Is Correct (Header PDF)
How to Check If Int Is Legal Enum in C#
Should I Worry About "This Async Method Lacks 'Await' Operators and Will Run Synchronously" Warning