Skip to content

Instantly share code, notes, and snippets.

@orlys
Last active April 25, 2025 09:53
Show Gist options
  • Save orlys/ef002073814feecd0904a7a1139c1a76 to your computer and use it in GitHub Desktop.
Save orlys/ef002073814feecd0904a7a1139c1a76 to your computer and use it in GitHub Desktop.
Let JsonElement supports dynamic member access similar to the dynamic type
// License: WTFPL
// Author: Orlys
// Year: 2024
namespace System.Text.Json;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Text.Json;
public sealed class DynamicJsonElement : DynamicObject
{
private readonly JsonElement _jsonElement;
private readonly DynamicJsonElement? _parent;
private readonly string _propertyName;
public static dynamic Deserialize([StringSyntax(StringSyntaxAttribute.Json)] string json)
{
return From(JsonSerializer.Deserialize<JsonElement>(json));
}
public static dynamic From(JsonElement jsonElement)
{
return new DynamicJsonElement(jsonElement, null, "$");
}
private DynamicJsonElement(JsonElement jsonElement, DynamicJsonElement? parent, string propertyName)
{
_jsonElement = jsonElement;
_parent = parent;
_propertyName = propertyName;
}
public string GetFullPath(bool includeRoot = true)
{
var stack = new Stack<string>();
for (var p = this; includeRoot ? p != null : p._parent != null; p = p._parent)
{
stack.Push(p._propertyName);
}
return string.Join(".", stack);
}
public override string ToString()
{
return _jsonElement.ToString();
}
private bool GetJsonProperty(IComparable comparable, Type returnType, out object? result)
{
if (_jsonElement.ValueKind is JsonValueKind.Object &&
comparable is string name &&
(_jsonElement.TryGetProperty(name.ToLowerInvariant(), out var jsonObject) ||
_jsonElement.TryGetProperty(name, out jsonObject)))
{
result = new DynamicJsonElement(jsonObject, this, name);
return true;
}
if (_jsonElement.ValueKind is JsonValueKind.Array &&
comparable is int index &&
GetJsonValue(_jsonElement, typeof(object), out var array))
{
result = (array as Array)?.GetValue(index);
return true;
}
return GetJsonValue(_jsonElement, returnType, out result);
}
private static Array EnumerateJsonArray(JsonElement jsonElement)
{
if (jsonElement.ValueKind is JsonValueKind.Array)
{
var jsonArray = jsonElement.EnumerateArray();
var array = Array.CreateInstance(typeof(object), jsonElement.GetArrayLength());
for (int i = 0; jsonArray.MoveNext(); i++)
{
if (GetJsonValue(jsonArray.Current, typeof(object), out var item))
{
array.SetValue(item, i);
}
}
return array;
}
return Array.Empty<object>();
}
private static bool GetJsonValue(JsonElement jsonElement, Type returnType, out object? result)
{
if (jsonElement.ValueKind is JsonValueKind.Object)
{
result = new DynamicJsonElement(jsonElement, null, null!);
return true;
}
if (jsonElement.ValueKind is JsonValueKind.Array)
{
result = EnumerateJsonArray(jsonElement);
return true;
}
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
result = null;
return true;
}
if (jsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False)
{
result = jsonElement.GetBoolean();
return true;
}
if (jsonElement.ValueKind is JsonValueKind.String)
{
result = jsonElement.GetString();
return true;
}
if (jsonElement.ValueKind is JsonValueKind.Number)
{
if (jsonElement.TryGetInt32(out var i4))
{
result = i4;
return true;
}
if (jsonElement.TryGetUInt32(out var u4))
{
result = u4;
return true;
}
if (jsonElement.TryGetInt64(out var i8))
{
result = i8;
return true;
}
if (jsonElement.TryGetUInt64(out var u8))
{
result = u8;
return true;
}
if (jsonElement.TryGetDouble(out var f8))
{
result = f8;
return true;
}
}
result = null;
return false;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
var names = _jsonElement.EnumerateObject().Select(x => x.Name);
return names;
}
public override bool TryConvert(ConvertBinder binder, out object? result)
{
return GetJsonValue(_jsonElement, binder.Type, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
return GetJsonProperty(binder.Name, binder.ReturnType, out result);
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
{
if (indexes is { Length: 1 } array)
{
if (array[0] is string name &&
GetJsonProperty(name, binder.ReturnType, out result))
{
return true;
}
if (array[0] is int index &&
GetJsonProperty(index, binder.ReturnType, out result))
{
return true;
}
}
return base.TryGetIndex(binder, indexes, out result);
}
}
@orlys
Copy link
Author

orlys commented Dec 10, 2024

dynamic dynObj = DynamicJsonElement.Deserialize("""
{
    "name": "dynamic-json-element",
    "team": {
        "id": 123,
        "owner": "alice",
        "members": [
            "bob",
           "clarie",
           {
               "who": "am i"
            }
        ],
        "tags": [ 123, "456", 789 ]
    }
}
""");

foreach (var item in dynObj["team"]["members"])
{
    if (item is string s)
        Console.WriteLine(s);
}
Console.WriteLine();
foreach (var item in dynObj.team.tags)
{
    if (item is int i)
        Console.WriteLine(i);
}
Console.WriteLine();
Console.WriteLine(dynObj["team"]["members"].GetFullPath());
Console.WriteLine(dynObj["team"]["owner"].GetFullPath(false));
Console.WriteLine(dynObj["team"]["members"][2]["who"]);


// === result ===
// bob
// clarie
//
// 123
// 789
// 
// $.team.members
// team.owner
// am i

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment