Last active
April 25, 2025 09:53
-
-
Save orlys/ef002073814feecd0904a7a1139c1a76 to your computer and use it in GitHub Desktop.
Let JsonElement supports dynamic member access similar to the dynamic type
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.