Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active May 15, 2022 13:39
Show Gist options
  • Save mrange/51935f0a0a3f43959432bda3d23793b7 to your computer and use it in GitHub Desktop.
Save mrange/51935f0a0a3f43959432bda3d23793b7 to your computer and use it in GitHub Desktop.
Dynamic JSON in C#

Dynamic JSON in C#

How to run

  1. Install dotnet: https://dotnet.microsoft.com/en-us/download
  2. Create a folder named for example: CsDymamic
  3. Create file in the folder named: CsDyamic.csproj and copy the content of 1_CsDyamic.csproj into that file
  4. Create file in the folder named: Program.cs and copy the content of 2_Program.cs below into that file
  5. Launch the application in Visual Studio or through the command line dotnet run from the folder CsDymamic
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
</PropertyGroup>
</Project>
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