- Install dotnet: https://dotnet.microsoft.com/en-us/download
- Create a folder named for example:
CsDymamic
- Create file in the folder named:
CsDyamic.csproj
and copy the content of1_CsDyamic.csproj
into that file - Create file in the folder named:
Program.cs
and copy the content of2_Program.cs
below into that file - Launch the application in Visual Studio or through the command line
dotnet run
from the folderCsDymamic
Last active
May 15, 2022 13:39
-
-
Save mrange/51935f0a0a3f43959432bda3d23793b7 to your computer and use it in GitHub Desktop.
Dynamic JSON in C#
This file contains 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
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net6.0</TargetFramework> | |
<ImplicitUsings>enable</ImplicitUsings> | |
<Nullable>enable</Nullable> | |
<EnablePreviewFeatures>True</EnablePreviewFeatures> | |
</PropertyGroup> | |
</Project> |
This file contains 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
using Example; | |
// This is a preview feature currently so make sure the project has: <EnablePreviewFeatures>True</EnablePreviewFeatures> | |
var json = """ | |
{ | |
"x" : 1, | |
"y" : "1234", | |
"z" : [1,2,3], | |
"w" : { "a":true, "b": null } | |
} | |
"""; | |
var root = JsonDynamic.FromJsonString(json); | |
// Query x as double | |
double x = root.x; | |
Console.WriteLine($"y:{x}"); | |
// Query y as double, it is stored as string but parsed into double | |
double y = root.y; | |
Console.WriteLine($"y:{y}"); | |
// Query a which doesn't exist. This is returned as an undefined value | |
string a = root.a; | |
Console.WriteLine($"a:{a}"); | |
// Query b and check that it exists | |
if (!root.b.IsUndefined()) | |
{ | |
string b = root.b; | |
Console.WriteLine($"b:{b}"); | |
} | |
// Query zs and interpret it as double array | |
double[] zs = root.z; | |
foreach (var z in zs) | |
{ | |
Console.WriteLine($"z:{z}"); | |
} | |
// Query w as object | |
dynamic w = root.w; | |
// Query w.a as boolean | |
bool wa = w.a; | |
Console.WriteLine($"w.a:{wa}"); | |
// Query w.b as string | |
string wb = w.b; | |
Console.WriteLine($"w.b:{wb}"); | |
// Query w.c and require it to exist | |
// it will throw here and the exception will show the path that was | |
// attempted: $.w.c | |
string wc = w.c.MustExist(); | |
Console.WriteLine($"w.c:{wc}"); | |
namespace Example | |
{ | |
using System; | |
using System.Buffers; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Dynamic; | |
using System.Globalization; | |
using System.Text; | |
using System.Text.Json; | |
using Path = PersistentList<IPathSegment>; | |
sealed partial record PersistentList<T>(T Head, PersistentList<T>? Tail) : IEnumerable<T> | |
{ | |
public IEnumerator<T> GetEnumerator() | |
{ | |
var c = this; | |
while (c is not null) | |
{ | |
yield return c.Head; | |
c = c.Tail; | |
} | |
} | |
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
} | |
static partial class PersistentList | |
{ | |
public static PersistentList<T> PrependWith<T>(this PersistentList<T>? tail, T head) => new(head, tail); | |
} | |
sealed partial class UndefinedJsonNodeException : Exception | |
{ | |
public Path? Path; | |
public UndefinedJsonNodeException() : base() | |
{ | |
} | |
public UndefinedJsonNodeException(string message) : base(message) | |
{ | |
} | |
public UndefinedJsonNodeException(string message, Exception inner) : base(message, inner) | |
{ | |
} | |
} | |
partial interface IPathSegment | |
{ | |
} | |
sealed partial class MemberSegment : IPathSegment | |
{ | |
public readonly string Member; | |
public MemberSegment(string member) | |
{ | |
Member = member; | |
} | |
} | |
sealed partial class IndexSegment : IPathSegment | |
{ | |
public readonly int Index; | |
public IndexSegment(int index) | |
{ | |
Index = index; | |
} | |
} | |
static partial class PathExtensions | |
{ | |
public static StringBuilder PrettyPrintPath(this StringBuilder sb, Path? path) | |
{ | |
if (path is null) | |
{ | |
return sb.Append('$'); | |
} | |
else | |
{ | |
var next = sb.PrettyPrintPath(path.Tail); | |
switch(path.Head) | |
{ | |
case MemberSegment s: | |
next.Append('.'); | |
next.Append(s.Member); | |
break; | |
case IndexSegment s: | |
next.Append('[').Append(s.Index).Append(']'); | |
break; | |
default: | |
next.Append("<UNKNOWN>"); | |
break; | |
} | |
return next; | |
} | |
} | |
public static string PrettyPrintPath(this Path path) | |
{ | |
var sb = new StringBuilder(16); | |
sb.PrettyPrintPath(path); | |
return sb.ToString(); | |
} | |
} | |
abstract partial class JsonNode : DynamicObject | |
{ | |
readonly Path? _path; | |
protected JsonNode(Path? path) | |
{ | |
_path = path; | |
} | |
public Path? Path => _path; | |
public bool IsNullOrUndefined() => IsUndefined()||IsNull(); | |
public JsonNode Member(string name) => Member(false, name); | |
public T DefaultTo<T>(T defaultValue) | |
{ | |
if (IsNullOrUndefined()) | |
{ | |
return defaultValue; | |
} | |
if (TryConvertTo(typeof(T), out object? result)) | |
{ | |
return (T)result!; | |
} | |
else | |
{ | |
return defaultValue; | |
} | |
} | |
public abstract int Count(); | |
public abstract bool IsUndefined(); | |
public abstract bool IsNull(); | |
public abstract bool IsArray(); | |
public abstract bool IsObject(); | |
public abstract bool IsString(); | |
public abstract bool IsNumber(); | |
public abstract bool IsBool(); | |
public abstract JsonNode MustExist(); | |
public abstract void WriteTo(Utf8JsonWriter writer); | |
public abstract JsonNode[] AsArray(); | |
public abstract string AsString(); | |
public abstract bool AsBool(); | |
public abstract double AsNumber(); | |
public abstract JsonNode Index(int index); | |
public abstract JsonNode Member(bool ignoreCase, string name); | |
public bool TryConvertTo(Type tp, out object? result) | |
{ | |
if (tp == typeof(object)) | |
{ | |
result = this; | |
return true; | |
} | |
else if (tp == typeof(string)) | |
{ | |
result = AsString(); | |
return true; | |
} | |
else if (tp == typeof(bool)) | |
{ | |
result = AsBool(); | |
return true; | |
} | |
else if (tp == typeof(double)) | |
{ | |
result = AsNumber(); | |
return true; | |
} | |
else if (tp == typeof(bool?)) | |
{ | |
result = IsNullOrUndefined() ? null : (bool?)AsBool(); | |
return true; | |
} | |
else if (tp == typeof(double?)) | |
{ | |
result = IsNullOrUndefined() ? null : (double?)AsNumber(); | |
return true; | |
} | |
else if (tp == typeof(object[])) | |
{ | |
result = AsArray(); | |
return true; | |
} | |
else if (tp == typeof(string[])) | |
{ | |
result = AsArray().Select(jn => jn.AsString()).ToArray(); | |
return true; | |
} | |
else if (tp == typeof(bool[])) | |
{ | |
result = AsArray().Select(jn => jn.AsBool()).ToArray(); | |
return true; | |
} | |
else if (tp == typeof(double[])) | |
{ | |
result = AsArray().Select(jn => jn.AsNumber()).ToArray(); | |
return true; | |
} | |
else | |
{ | |
result = null; | |
return false; | |
} | |
} | |
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result) | |
{ | |
if (indexes.Length != 1) | |
{ | |
// TODO: Throw? | |
result = null; | |
return false; | |
} | |
result = Index((int)indexes[0]); | |
return true; | |
} | |
public override bool TryGetMember(GetMemberBinder binder, out object result) | |
{ | |
result = Member(binder.IgnoreCase, binder.Name); | |
return true; | |
} | |
public override bool TryConvert(ConvertBinder binder, out object? result) | |
{ | |
return TryConvertTo(binder.Type, out result); | |
} | |
} | |
static partial class JsonDynamic | |
{ | |
static readonly Encoding _utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); | |
abstract partial class JsonScalar : JsonNode | |
{ | |
protected JsonScalar(Path? path) : base(path) {} | |
public override int Count() => 0; | |
public override JsonNode[] AsArray() => Array.Empty<JsonNode>(); | |
public override JsonNode Index(int index) => new JsonUndefined(Path.PrependWith(new IndexSegment(index))); | |
public override JsonNode Member(bool ignoreCase, string name) => new JsonUndefined(Path.PrependWith(new MemberSegment(name))); | |
} | |
abstract partial class JsonCollection : JsonNode | |
{ | |
protected JsonCollection(Path? path) : base(path) {} | |
} | |
sealed partial class JsonArray : JsonCollection, IEnumerable<JsonNode> | |
{ | |
readonly JsonNode[] _vs; | |
public JsonArray(Path? path, JsonNode[] vs) : base(path) | |
{ | |
_vs = vs; | |
} | |
public override int Count() => _vs.Length; | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => false; | |
public override bool IsArray() => true; | |
public override bool IsObject() => false; | |
public override bool IsString() => false; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) | |
{ | |
writer.WriteStartArray(); | |
foreach (var v in _vs) | |
{ | |
if (!v.IsUndefined()) | |
{ | |
v.WriteTo(writer); | |
} | |
} | |
writer.WriteEndArray(); | |
} | |
public override JsonNode[] AsArray() => _vs; | |
public override bool AsBool() => true; | |
public override double AsNumber() => double.NaN; | |
public override string AsString() => "[array]"; | |
public override JsonNode Index(int index) => | |
index >= 0 && index < _vs.Length | |
? _vs[index] | |
: new JsonUndefined(Path.PrependWith(new IndexSegment(index))) | |
; | |
public override JsonNode Member(bool ignoreCase, string name) => new JsonUndefined(Path.PrependWith(new MemberSegment(name))); | |
public IEnumerator<JsonNode> GetEnumerator() | |
{ | |
foreach(var v in _vs) | |
{ | |
yield return v; | |
} | |
} | |
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
} | |
sealed partial class JsonObject : JsonCollection, IEnumerable<(string, JsonNode)> | |
{ | |
readonly (string, JsonNode)[] _vs ; | |
readonly Dictionary<string, JsonNode> _dic; | |
public JsonObject(Path? path, (string, JsonNode)[] vs) : base(path) | |
{ | |
_vs = vs; | |
_dic = _vs.ToDictionary(kv => kv.Item1, kv => kv.Item2); | |
} | |
public override int Count() => _vs.Length; | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => false; | |
public override bool IsArray() => false; | |
public override bool IsObject() => true; | |
public override bool IsString() => false; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) | |
{ | |
writer.WriteStartObject(); | |
foreach (var (k, v) in _vs) | |
{ | |
if (!v.IsUndefined()) | |
{ | |
writer.WritePropertyName(k); | |
v.WriteTo(writer); | |
} | |
} | |
writer.WriteEndObject(); | |
} | |
public override JsonNode[] AsArray() => _vs.Select(kv => kv.Item2).ToArray(); | |
public override bool AsBool() => true; | |
public override double AsNumber() => double.NaN; | |
public override string AsString() => "[object]"; | |
public override JsonNode Index(int index) => | |
index >= 0 && index < _vs.Length | |
? _vs[index].Item2 | |
: new JsonUndefined(Path.PrependWith(new IndexSegment(index))) | |
; | |
public override JsonNode Member(bool ignoreCase, string name) => | |
_dic.TryGetValue(name, out JsonNode? node) | |
? node | |
: new JsonUndefined(Path.PrependWith(new MemberSegment(name))) | |
; | |
public override IEnumerable<string> GetDynamicMemberNames() => | |
_vs.Select(kv => kv.Item1) | |
; | |
public IEnumerator<(string, JsonNode)> GetEnumerator() | |
{ | |
foreach(var kv in _vs) | |
{ | |
yield return kv; | |
} | |
} | |
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |
} | |
sealed partial class JsonUndefined : JsonScalar | |
{ | |
public JsonUndefined(Path? path) : base(path) {} | |
public override bool IsUndefined() => true; | |
public override bool IsNull() => false; | |
public override bool IsArray() => false; | |
public override bool IsObject() => false; | |
public override bool IsString() => false; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() | |
{ | |
var sb = new StringBuilder(128) | |
.Append("Json value not found: ") | |
.PrettyPrintPath(Path) | |
; | |
throw new UndefinedJsonNodeException(sb.ToString()) | |
{ | |
Path = Path, | |
}; | |
} | |
public override void WriteTo(Utf8JsonWriter writer) => writer.WriteNullValue(); | |
public override bool AsBool() => false; | |
public override double AsNumber() => double.NaN; | |
public override string AsString() => "[undefined]"; | |
} | |
sealed partial class JsonNull : JsonScalar | |
{ | |
public JsonNull(Path? path) : base(path) {} | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => true; | |
public override bool IsArray() => false; | |
public override bool IsObject() => false; | |
public override bool IsString() => false; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) => writer.WriteNullValue(); | |
public override bool AsBool() => false; | |
public override double AsNumber() => 0.0; | |
public override string AsString() => ""; | |
} | |
sealed partial class JsonBool : JsonScalar | |
{ | |
readonly bool _v; | |
public JsonBool(Path? path, bool v) : base(path) | |
{ | |
_v = v; | |
} | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => false; | |
public override bool IsArray() => false; | |
public override bool IsObject() => false; | |
public override bool IsString() => false; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => true; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) => writer.WriteBooleanValue(_v); | |
public override bool AsBool() => _v; | |
public override double AsNumber() => _v ? 1.0 : 0.0; | |
public override string AsString() => _v ? "true" : "false"; | |
} | |
sealed partial class JsonNumber : JsonScalar | |
{ | |
readonly double _v; | |
public JsonNumber(Path? path, double v) : base(path) | |
{ | |
_v = v; | |
} | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => false; | |
public override bool IsArray() => false; | |
public override bool IsObject() => false; | |
public override bool IsString() => false; | |
public override bool IsNumber() => true; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) => writer.WriteNumberValue(_v); | |
public override bool AsBool() => !(_v == 0.0 || double.IsNaN(_v)); | |
public override double AsNumber() => _v; | |
public override string AsString() => _v.ToString(CultureInfo.InvariantCulture); | |
} | |
sealed partial class JsonString : JsonScalar | |
{ | |
readonly string _v; | |
public JsonString(Path? path, string v) : base(path) | |
{ | |
_v = v; | |
} | |
public override bool IsUndefined() => false; | |
public override bool IsNull() => false; | |
public override bool IsArray() => false; | |
public override bool IsObject() => false; | |
public override bool IsString() => true; | |
public override bool IsNumber() => false; | |
public override bool IsBool() => false; | |
public override JsonNode MustExist() => this; | |
public override void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(_v); | |
public override bool AsBool() => _v.Length > 0; | |
public override double AsNumber() => double.TryParse(_v, NumberStyles.Float, CultureInfo.InvariantCulture, out double d) ? d : double.NaN; | |
public override string AsString() => _v; | |
} | |
abstract partial class Builder | |
{ | |
public Path? Path = null; | |
public abstract Path? NestedPath(); | |
public abstract void Key(string key); | |
public abstract void Add(JsonNode node); | |
public abstract JsonNode Create(Stack<ArrayBuilder> arrayBuilders, Stack<ObjectBuilder> objectBuilders); | |
} | |
sealed partial class RootBuilder : Builder | |
{ | |
JsonNode? _root = null; | |
public override Path? NestedPath() => Path; | |
public override void Key(string key) => throw new InvalidOperationException($"Key is unexpected, key:{key}"); | |
public override void Add(JsonNode node) | |
{ | |
if (!(_root is null)) | |
{ | |
throw new InvalidOperationException("Root is expected to be empty"); | |
} | |
_root = node; | |
} | |
public override JsonNode Create(Stack<ArrayBuilder> arrayBuilders, Stack<ObjectBuilder> objectBuilders) | |
{ | |
if (_root is null) | |
{ | |
throw new InvalidOperationException("Root is not expected to be empty"); | |
} | |
return _root; | |
} | |
} | |
sealed partial class ArrayBuilder : Builder | |
{ | |
readonly List<JsonNode> _vs = new(8); | |
public override Path? NestedPath() => Path.PrependWith(new IndexSegment(_vs.Count)); | |
public override void Key(string key) => throw new InvalidOperationException($"Key is unexpected, key:{key}"); | |
public override void Add(JsonNode node) | |
{ | |
_vs.Add(node); | |
} | |
public override JsonNode Create(Stack<ArrayBuilder> arrayBuilders, Stack<ObjectBuilder> objectBuilders) | |
{ | |
var jn = new JsonArray(Path, _vs.ToArray()); | |
Path = null; | |
_vs.Clear(); | |
arrayBuilders.Push(this); | |
return jn; | |
} | |
} | |
sealed partial class ObjectBuilder : Builder | |
{ | |
string? _key = null; | |
readonly List<(string, JsonNode)> _vs = new(8); | |
public override Path? NestedPath() => Path.PrependWith(new MemberSegment(_key!)); | |
public override void Key(string key) | |
{ | |
_key = key; | |
} | |
public override void Add(JsonNode node) | |
{ | |
if (_key is null) | |
{ | |
throw new InvalidOperationException($"Key is unexpectedly not set"); | |
} | |
_vs.Add((_key, node)); | |
_key = null; | |
} | |
public override JsonNode Create(Stack<ArrayBuilder> arrayBuilders, Stack<ObjectBuilder> objectBuilders) | |
{ | |
var jn = new JsonObject(Path, _vs.ToArray()); | |
_key = null; | |
Path = null; | |
_vs.Clear(); | |
objectBuilders.Push(this); | |
return jn; | |
} | |
} | |
public static string ToJsonString(JsonNode jn, bool indented = true) | |
{ | |
var buffer = new ArrayBufferWriter<byte>(256); | |
var options = new JsonWriterOptions() | |
{ | |
Indented = indented, | |
SkipValidation = false , | |
}; | |
var writer = new Utf8JsonWriter(buffer, options); | |
jn.WriteTo(writer); | |
writer.Flush(); | |
return _utf8.GetString(buffer.WrittenSpan); | |
} | |
public static dynamic FromJsonString(string s) | |
{ | |
var bs = _utf8.GetBytes(s); | |
return FromBytes(bs); | |
} | |
public static dynamic FromBytes(byte[] bs) | |
{ | |
var options = new JsonReaderOptions() | |
{ | |
AllowTrailingCommas = true , | |
CommentHandling = JsonCommentHandling.Skip, | |
}; | |
var r = new Utf8JsonReader(bs, options); | |
return FromReader(r); | |
} | |
public static dynamic FromReader(Utf8JsonReader reader) | |
{ | |
var arrayBuilders = new Stack<ArrayBuilder>(16); | |
var objectBuilders = new Stack<ObjectBuilder>(16); | |
var stack = new Stack<Builder>(16); | |
Builder current = new RootBuilder(); | |
while(reader.Read()) | |
{ | |
switch(reader.TokenType) | |
{ | |
case JsonTokenType.StartObject: | |
var objectPath = current.NestedPath(); | |
stack.Push(current); | |
if(objectBuilders.Count > 0) | |
{ | |
current = objectBuilders.Pop(); | |
} | |
else | |
{ | |
current = new ObjectBuilder(); | |
} | |
current.Path = objectPath; | |
break; | |
case JsonTokenType.EndObject: | |
var objectValue = current.Create(arrayBuilders, objectBuilders); | |
current = stack.Pop(); | |
current.Add(objectValue); | |
break; | |
case JsonTokenType.StartArray: | |
var arrayPath = current.NestedPath(); | |
stack.Push(current); | |
if(arrayBuilders.Count > 0) | |
{ | |
current = arrayBuilders.Pop(); | |
} | |
else | |
{ | |
current = new ArrayBuilder(); | |
} | |
current.Path = arrayPath; | |
break; | |
case JsonTokenType.EndArray: | |
var arrayValue = current.Create(arrayBuilders, objectBuilders); | |
current = stack.Pop(); | |
current.Add(arrayValue); | |
break; | |
case JsonTokenType.PropertyName: | |
current.Key(reader.GetString()??""); | |
break; | |
case JsonTokenType.String: | |
current.Add(new JsonString(current.NestedPath(), reader.GetString()??"")); | |
break; | |
case JsonTokenType.Number: | |
current.Add(new JsonNumber(current.NestedPath(), reader.GetDouble())); | |
break; | |
case JsonTokenType.True: | |
current.Add(new JsonBool(current.NestedPath(), true)); | |
break; | |
case JsonTokenType.False: | |
current.Add(new JsonBool(current.NestedPath(), false)); | |
break; | |
case JsonTokenType.Null: | |
current.Add(new JsonNull(current.NestedPath())); | |
break; | |
// Ignore these tokens | |
case JsonTokenType.Comment: | |
case JsonTokenType.None: | |
default: | |
break; | |
} | |
} | |
if (current is RootBuilder rb) | |
{ | |
return rb.Create(arrayBuilders, objectBuilders); | |
} | |
else | |
{ | |
throw new InvalidOperationException("Current builder is expected to be a root builder"); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment