using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
[JsonConverter(typeof(TypedExtensionDataConverter))]
public class Root<TKey, TValue> where TValue : DynamicKey where TKey : notnull
public string id { get; set; }
public Properties properties { get; set; }
public Dictionary<TKey, TValue> DynamicValues { get; init; } = new();
public string createtime { get; set; }
public abstract class DynamicKey;
public class IdAndMetadata : DynamicKey
public string id { get; set; }
public Metadata metadata { get; set; }
public string metadata0 { get; set; }
public string metadata1 { get; set; }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonTypedExtensionDataAttribute : Attribute;
public class TypedExtensionDataConverter : JsonConverter
public override bool CanConvert(Type objectType) => throw new NotImplementedException();
static JsonProperty GetExtensionJsonProperty(JsonObjectContract contract)
return contract.Properties.Where(p => p.PropertyName != null && p.AttributeProvider?.GetAttributes(typeof(JsonTypedExtensionDataAttribute), false).Any() == true).Single();
catch (InvalidOperationException ex)
throw new JsonSerializationException(string.Format("Exactly one property with JsonTypedExtensionDataAttribute is required for type {0}", contract.UnderlyingType), ex);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
if (!(serializer.ContractResolver.ResolveContract(objectType) is JsonObjectContract contract))
throw new JsonSerializationException($"Contract for {objectType} is not a JsonObjectContract");
if (!(contract.DefaultCreator is {} creator))
throw new JsonSerializationException($"No default creator found for {objectType}");
var extensionJsonProperty = GetExtensionJsonProperty(contract);
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
var jObj = JObject.Load(reader);
var extensionJProperty = (JProperty?)null;
for (int i = jObj.Count - 1; i >= 0; i--)
var property = (JProperty)jObj.AsList()[i];
if (contract.Properties.GetClosestMatchProperty(property.Name) == null)
if (extensionJProperty == null)
extensionJProperty = new JProperty(extensionJsonProperty.PropertyName!, new JObject());
jObj.Add(extensionJProperty);
((JObject)extensionJProperty.Value).Add(property.RemoveFromLowestPossibleParent());
var value = existingValue ?? creator();
using (var subReader = jObj.CreateReader())
serializer.Populate(subReader, value);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
if (!(serializer.ContractResolver.ResolveContract(value.GetType()) is JsonObjectContract contract))
throw new JsonSerializationException($"Contract for {value.GetType()} is not a JsonObjectContract");
var extensionJsonProperty = GetExtensionJsonProperty(contract);
using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
jObj = JObject.FromObject(value!, serializer);
var extensionValue = (jObj[extensionJsonProperty.PropertyName!] as JObject)?.RemoveFromLowestPossibleParent();
if (extensionValue != null)
for (int i = extensionValue.Count - 1; i >= 0; i--)
var property = (JProperty)extensionValue.AsList()[i];
jObj.Add(property.RemoveFromLowestPossibleParent());
bool Disabled { get { return disabled; } set { disabled = value; } }
public override bool CanWrite => !Disabled;
public override bool CanRead => !Disabled;
public static partial class JsonExtensions
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
ArgumentNullException.ThrowIfNull(reader);
if (reader.TokenType == JsonToken.None)
while (reader.TokenType == JsonToken.Comment)
public static JsonReader ReadAndAssert(this JsonReader reader)
ArgumentNullException.ThrowIfNull(reader);
throw new JsonReaderException("Unexpected end of JSON stream.");
public static TJToken? RemoveFromLowestPossibleParent<TJToken>(this TJToken? node) where TJToken : JToken
var property = node.Parent as JProperty;
if (toRemove.Parent != null)
public static IList<JToken> AsList(this IList<JToken> container) => container;
internal struct PushValue<T> : IDisposable
public PushValue(T value, Func<T> getValue, Action<T> setValue)
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
public void Dispose() => setValue?.Invoke(oldValue);
public static void Test()
"createtime": "datetime format"
var root = JsonConvert.DeserializeObject<Root<string, IdAndMetadata>>(json);
var settings = new JsonSerializerSettings
NullValueHandling = NullValueHandling.Ignore,
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Console.WriteLine("Deserialized and re-serialized {0}:", root);
Console.WriteLine(json2);
Assert.That(JToken.DeepEquals(JsonConvert.DeserializeObject<JToken>(json), JsonConvert.DeserializeObject<JToken>(json2)));
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Namespace, typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");