Skip to content

Commit e09c99a

Browse files
committed
jsonnode's weird api
1 parent da3fd31 commit e09c99a

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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

Comments
 (0)