Skip to content

Commit 75140f2

Browse files
committed
a common pitfall
1 parent d09fd34 commit 75140f2

File tree

1 file changed

+95
-0
lines changed

1 file changed

+95
-0
lines changed
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: "A Common Pitfall of Working with JsonNode"
3+
date: 2024-10-26 09:00:00 +1200
4+
tags: [.net, json, system.text.json, learning]
5+
toc: true
6+
pin: false
7+
---
8+
9+
When anyone publishes a work of creativity, they invite both praise and criticism. But open source development has a special third category: bug reports. Sometimes, these "bugs" are really just user error.
10+
11+
In this post I'm going to review what is arguably the most common of these cases: failing to parse string-encoded JSON data.
12+
13+
## The `JsonNode` model
14+
15+
All of the `json-everything` libraries operate on the `JsonNode` family of models from _System.Text.Json_. These models offer a remarkable feature that makes inlining JSON data very simple: implicit casts into `JsonValue` from compatible .Net types.
16+
17+
So, C# `bool` maps to the `true` and `false` JSON literals, [`null` maps to the `null` JSON literal](./null-has-value-too), `double` and all of the other numeric types map to JSON numbers, and `string` maps to JSON strings. That means the compiler considers all of the following code as valid and performs the appropriate conversion in the background:
18+
19+
```c#
20+
JsonNode jsonBool = false;
21+
// in modern C#, you need to qualify that a var can be nullable
22+
JsonNode? jsonNull = null;
23+
JsonNode jsonNumber = 42;
24+
JsonNode jsonString = "my string data"
25+
```
26+
27+
> The cast itself results in a `JsonValue`, which inherits from `JsonNode`. `JsonObject` and `JsonArray` also derive from `JsonNode`.
28+
{: .prompt-info }
29+
30+
What this enables is a very intuitive approach to building complex JSON in a way that, if you squint just right, looks like the JSON syntax itself:
31+
32+
```c#
33+
// e.g. data for a person
34+
var jsonObject = new JsonObject
35+
{
36+
["name"] = "Ross",
37+
["age"] = 25,
38+
["married"] = false,
39+
["friends"] = new JsonArray
40+
{
41+
"Rachel",
42+
"Chandler",
43+
"Phoebe",
44+
"Joey",
45+
"Monica"
46+
}
47+
}
48+
```
49+
50+
However one of these conversions creates a perfect storm for confusion.
51+
52+
## Falling into the trap
53+
54+
> I'm going to use `JsonE.Evaluate()` for illustration, but since basically all of the `json-everything` libraries expose methods which take `JsonNode` as a parameter, this pitfall applies to them all.
55+
{: .prompt-warning }
56+
57+
Getting straight to the point, the error I see a lot of people making is passing the JSON data as a string into methods that have `JsonNode` parameters.
58+
59+
```c#
60+
var template = """
61+
{
62+
"$flatten": [
63+
[1, 2],
64+
[3, 4],
65+
[[5]]
66+
]
67+
}
68+
"""
69+
var result = JsonE.Evaluate(template);
70+
```
71+
72+
These users expect that the template will be interpreted as JSON and processed accordingly, giving the JSON result of `[1, 2, 3, 4, 5]`. Instead they just get the template back. Then, because it's not working, they file a bug. (Some people create a "question" issue, but most people assume something is wrong with the lib.)
73+
74+
Since the compiler, which is supposed to provide guardrails against incorrect typing, reports that everything is fine, they assume the problem must be with the library. But in this case, `JsonNode`'s implicit cast has subverted the compiler's type-checking in the name of providing a service (easy, JSON-like, inline data building).
75+
76+
## The solution
77+
78+
The user just needs to parse the string-encoded JSON into the `JsonNode` model, and then pass _that_ into the `JsonE.Evaluate()` method.
79+
80+
This can be done in multiple ways, but the primary ones I would use (in order) are:
81+
82+
1. `JsonNode.Parse(jsonText)`
83+
2. `JsonSerializer.Deserialize<JsonNode>(jsonText)`
84+
85+
Both of these will give you a `JsonNode`. The second is a bit indirect, but it gets the job done, and I'm pretty sure it just ends up calling the first.
86+
87+
## What can be done?
88+
89+
I don't really think anything can be done aside from educating users of _System.Text.Json_. It's an API decision, and frankly one that I agree with. When I first built _Manatee.Json_ almost ten years ago, I started with only a JSON DOM that very closely resembled `JsonNode`, including all of the same implicit casts.
90+
91+
It's a very useful API, but it does require knowledge that the cast is happening.
92+
93+
In the end, I assume many of the users who fall into this trap and report a "bug" (or open a question issue) are likely just new to .Net. Whatever the reason, the best approach to addressing these cases is maintaining an attitude of helpfulness, understanding, and education.
94+
95+
_If you like the work I put out, and would like to help ensure that I keep it up, please consider [becoming a sponsor](https://github.com/sponsors/gregsdennis)!_

0 commit comments

Comments
 (0)