|
| 1 | +--- |
| 2 | +title: "JsonNode's Odd API" |
| 3 | +date: 2023-07-26 09:00:00 +1200 |
| 4 | +tags: [json-path, json-pointer] |
| 5 | +toc: true |
| 6 | +pin: false |
| 7 | +--- |
| 8 | + |
| 9 | +```c# |
| 10 | +var array = new JsonArray |
| 11 | +{ |
| 12 | + ["a"] = 1, |
| 13 | + ["b"] = 2, |
| 14 | + ["c"] = 3, |
| 15 | +}; |
| 16 | +``` |
| 17 | + |
| 18 | +This compiles. Why does this compile?! |
| 19 | + |
| 20 | +Today we're going to explore that. |
| 21 | + |
| 22 | +## What's wrong? |
| 23 | + |
| 24 | +In case you didn't see it, we're creating a `JsonArray` instance and initializing using key/value pairs. But arrays don't contain key/value pairs; they contain values. Objects contain key/value pairs. |
| 25 | + |
| 26 | +```c# |
| 27 | +var list = new List<int> |
| 28 | +{ |
| 29 | + ["a"] = 1, |
| 30 | + ["b"] = 2, |
| 31 | + ["c"] = 3, |
| 32 | +}; |
| 33 | +``` |
| 34 | + |
| 35 | +This doesn't compile, as one would expect. So why does `JsonArray` allow this? Is the collection initializer broken? |
| 36 | + |
| 37 | +## Collection initializers |
| 38 | + |
| 39 | +Microsoft actually has some really good [documentation](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers) on collection initializers so I'm not going to dive into it here. Have a read through that if you like. |
| 40 | + |
| 41 | +The crux of it comes down to when collection initializers are allowed. First, you need to implement `IEnumerable<T>` and an `.Add(T)` method (apparently it also works as an extension method). This will enable the basic collection initializer syntax, like |
| 42 | + |
| 43 | +```c# |
| 44 | +var list = new List<int> { 1, 2, 3 }; |
| 45 | +``` |
| 46 | + |
| 47 | +But you can also enable direct-indexing initialization by adding an indexer. This lets us do thing like |
| 48 | + |
| 49 | +```c# |
| 50 | +var list = new List<int>(10) |
| 51 | +{ |
| 52 | + [2] = 1, |
| 53 | + [5] = 2, |
| 54 | + [6] = 3 |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +More commonly, you may see this used for `Dictionary<TKey, TValue>` initialization: |
| 59 | + |
| 60 | +```c# |
| 61 | +var dict = new Dictionary<string, int> |
| 62 | +{ |
| 63 | + ["a"] = 1, |
| 64 | + ["b"] = 2, |
| 65 | + ["c"] = 3, |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +But, wait... does that mean that `JsonArray` has a string indexer? |
| 70 | + |
| 71 | +## `JsonArray` has a string indexer! |
| 72 | + |
| 73 | +It sure does! You can see it in the documentation, right there under [Properties](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonarray?view=net-7.0#properties). |
| 74 | +
|
| 75 | +Why?! Why would you define a string indexer on an array type? |
| 76 | + |
| 77 | +Well, they didn't. They [defined](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonnode?view=net-7.0#properties) it and the integer indexer on the base type, `JsonNode`, as a convenience for people working directly with the base type without having to cast it to a `JsonArray` or `JsonObject` first. |
| 78 | + |
| 79 | +But now, all of the `JsonNode`-derived types have both an integer indexer and a string indexer, and it's really weird. It makes all of this code completely valid: |
| 80 | + |
| 81 | +```c# |
| 82 | +JsonValue number = ((JsonNode)5).AsValue(); // can't cast directly to JsonValue |
| 83 | +_ = number[5]; // compiles but will explode |
| 84 | +_ = number["five"]; // compiles but will explode |
| 85 | +
|
| 86 | +JsonArray array = new() { 0, 1, 2, 3, 4, 5, 6 }; |
| 87 | +_ = array[5]; // fine |
| 88 | +_ = array["five"]; // compiles but will explode |
| 89 | +
|
| 90 | +JsonObject obj = new() { ["five"] = 1 }; |
| 91 | +_ = obj[5]; // compiles but will explode |
| 92 | +_ = obj["five"]; // fine |
| 93 | +``` |
| 94 | + |
| 95 | +## Is this useful? |
| 96 | + |
| 97 | +This seems like a very strange API design decision to me. I don't think I'd ever trust a `JsonNode` enough to confidently attempt to index it before checking to see if it _can_ be indexed. Furthermore, the process of checking whether it can be indexed can easily result in a correctly-typed variable. |
| 98 | + |
| 99 | +```c# |
| 100 | +if (node is JsonArray array) |
| 101 | + Console.WriteLine(array[5]); |
| 102 | +``` |
| 103 | + |
| 104 | +This will probably explode because I didn't check bounds, but from a type safety point of view, this is SO much better. |
| 105 | + |
| 106 | +I have no need to access indexed values directly from a `JsonNode`. I think this API enables programming techniques that are dangerously close to using the `dynamic` keyword, which should be [avoided at all costs](https://www.youtube.com/watch?v=VyGAEbmiWjE). |
0 commit comments