Updated on 2024-04-04
If you're putting together templates or other messes using JSON and C# this may be the ticket for you.
I don't like the polarity mismatch between things like JSON and static typed languages. In Javascript for example, you can access JSON data as though they were native objects - because essentially they are in Javascript. With C# you typically have to use a DOM or parser of some sort to navigate and access data in your JSON infoset. This can hinder readability and make your code awkward.
This restriction isn't strictly necessary in C#, since it has the dynamic keyword which can be used to late bind. Unfortunately you lose static typing in the process. Due to that, I've made the JSON objects read-only so that it isn't a free for all. The primary use of these would be in things like ASP.NET pages, as I'll demonstrate.
First, copy the following into a new file in your project:
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Text;
using System.Text.Json;
class DynamicJsonElement : DynamicObject, IEnumerable<object?>
{
JsonElement _inner;
private static object? ElemToObj(JsonElement elem)
{
object? result = null;
switch (elem.ValueKind)
{
case JsonValueKind.String:
result = elem.GetString()!;
break;
case JsonValueKind.Number:
result = elem.GetDouble();
break;
case JsonValueKind.Null:
result = null;
break;
case JsonValueKind.True:
result = true; break;
case JsonValueKind.False:
result = false; break;
case JsonValueKind.Object:
case JsonValueKind.Array:
result = new DynamicJsonElement(elem);
break;
default:
result = null;
break;
}
return result;
}
public DynamicJsonElement(JsonElement json)
{
_inner = json;
}
public int Count
{
get
{
if (_inner.ValueKind == JsonValueKind.Array)
{
return _inner.GetArrayLength();
}
throw new NotSupportedException();
}
}
public object? this[int index]
{
get
{
if (_inner.ValueKind == JsonValueKind.Array)
{
return ElemToObj(_inner[index]);
}
throw new NotSupportedException();
}
}
public override bool TryGetMember(
GetMemberBinder binder, out object? result)
{
JsonElement elem;
if (_inner.TryGetProperty(binder.Name, out elem))
{
result = ElemToObj(elem);
return true;
}
result = null;
return false;
}
public override bool TrySetMember(
SetMemberBinder binder, object? value)
{
return false;
}
public IEnumerator<object?> GetEnumerator()
{
if (_inner.ValueKind == JsonValueKind.Array)
{
foreach (var elem in _inner.EnumerateArray())
{
yield return ElemToObj(elem);
}
}
else
{
throw new NotSupportedException();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Now, whenever you receive a JsonElement back you can wrap it with new DynamicJsonElement(myJson).
dynamic root = new DynamicJsonElement(JsonDocument.Parse(stream).RootElement);
From there root is the root of the document, and you can access fields and values as you would in Javascript:
Console.WriteLine("{0} {1}",root.first_name,root.last_name);
Console.WriteLine("City: {0}",root.addresses[0].city);
Remember that you'll have to cast your values you get back because it returns object from its properties and accessors. Above we didn't have to because WriteLine() will take objects.
This is particularly useful in T4 and ASP.NET templates:
<span>Age: <%=root.age%></span>
The result is quite a bit more readable.
One wrinkle is when you foreach you must reassign the iterated values to a dynamic object as shown:
foreach(var item in root.addresses) {
dynamic address = item;
// now you can access address
}