|
| 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