using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)]
public sealed class JsonCanPopulateAttribute : System.Attribute
public JsonCanPopulateAttribute(bool canPopulate) => this.CanPopulate = canPopulate;
public bool CanPopulate { get; init; }
public class AutomaticArrayMergeConverter : ArrayMergeConverter
static Lazy<IContractResolver> DefaultResolver { get; } = new (() => JsonSerializer.Create().ContractResolver );
readonly IContractResolver resolver;
public AutomaticArrayMergeConverter() : this(DefaultResolver.Value) { }
public AutomaticArrayMergeConverter(IContractResolver resolver) => this.resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
public override bool CanConvert(Type objectType) =>
objectType.IsArray && objectType.GetArrayRank() == 1 && resolver.CanPopulateType(objectType.GetElementType()!);
public class ArrayMergeConverter : JsonConverter
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
throw new JsonSerializationException(string.Format("Non-array type {0} not supported.", objectType));
var contract = (JsonArrayContract)serializer.ContractResolver.ResolveContract(objectType);
if (contract.IsMultidimensionalArray)
throw new JsonSerializationException("Multidimensional arrays not supported.");
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
else if (reader.TokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Invalid start token: {0}", reader.TokenType));
var existingArray = existingValue as System.Array;
IList list = new List<object>();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
switch (reader.TokenType)
var existingItem = existingArray != null && list.Count < existingArray.Length ? existingArray.GetValue(list.Count) : null;
if (existingItem == null)
existingItem = serializer.Deserialize(reader, contract.CollectionItemType);
serializer.Populate(reader, existingItem);
var array = (existingArray != null && existingArray.Length == list.Count ? existingArray : Array.CreateInstance(contract.CollectionItemType!, list.Count));
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
public override bool CanConvert(Type objectType) => throw new NotImplementedException("This converter is meant to be applied via attributes only.");
public static partial class JsonExtensions
public static bool CanPopulateType(this IContractResolver resolver, Type type)
if (type.GetCustomAttribute<JsonCanPopulateAttribute>() is {} attr)
var elementContract = resolver.ResolveContract(type);
if (elementContract.Converter != null)
if (elementContract is JsonObjectContract c)
return c.Properties.All(p => p.Readable == p.Writable);
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None)
while (reader.TokenType == JsonToken.Comment)
public static JsonReader ReadAndAssert(this JsonReader reader)
throw new ArgumentNullException();
throw new JsonReaderException("Unexpected end of JSON stream.");
public class SerializeFieldAttribute : System.Attribute { }
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
public class GameObject { }
public class ManagedSubData { }
public class BuildableControllerV2 { }
public class NoPropertiesContractResolver : DefaultContractResolver { }
public class BuildableData : ManagedSubData
public BuildableData(BuildableControllerV2 prefab, float heightMove, bool isStatic, int initialQuantity, bool hasMinigame, UpgradeLevel[] upgradeLevels) =>
(this.prefab, this.heightMove, this.isStatic, this.initialQuantity, this.hasMinigame, this.upgradeLevels) = (prefab, heightMove, isStatic, initialQuantity, hasMinigame, upgradeLevels);
[SerializeField, JsonIgnore] private BuildableControllerV2 prefab;
[SerializeField, JsonProperty] private float heightMove = 2f;
[SerializeField, JsonProperty] private bool isStatic;
[SerializeField, JsonProperty] private int initialQuantity = 1;
[SerializeField, JsonProperty] private bool hasMinigame = false;
[SerializeField, JsonProperty(TypeNameHandling = TypeNameHandling.None)]
private UpgradeLevel[] upgradeLevels;
public (BuildableControllerV2 prefab, float heightMove, bool isStatic, int initialQuantity, bool hasMinigame, UpgradeLevel[] upgradeLevels) GetData() =>
(this.prefab, this.heightMove, this.isStatic, this.initialQuantity, this.hasMinigame, this.upgradeLevels);
[Serializable, JsonCanPopulate(true)]
public class UpgradeLevel
public UpgradeLevel(GameObject visual, int cost, Vector3 test) =>
(this.visual, this.cost, this.test) = (visual, cost, test);
[SerializeField] private GameObject visual;
[SerializeField, JsonProperty] private int cost;
[SerializeField, JsonProperty] private Vector3 test;
public (GameObject visual, int cost, Vector3 test) GetData() =>
(this.visual, this.cost, this.test);
public static void Test()
BuildableControllerV2 prefab = new();
GameObject visual = new();
var upgradeLevels1 = new UpgradeLevel []
new(visual, 25, new Vector3 { X = 101, Y = 202, Z = 303 }),
new(visual, 18, new Vector3 { X = 45, Y = 56, Z = 67 }),
var data1 = new BuildableData(prefab, 101.01f, true, 12, true, upgradeLevels1);
var resolver = new NoPropertiesContractResolver();
var converter = new AutomaticArrayMergeConverter(resolver);
var defaultSettings = new JsonSerializerSettings(){
Converters = { converter },
Formatting = Formatting.None,
PreserveReferencesHandling = PreserveReferencesHandling.None,
MissingMemberHandling = MissingMemberHandling.Ignore,
TypeNameHandling = TypeNameHandling.All,
ContractResolver = resolver,
var json = JsonConvert.SerializeObject(data1, Formatting.Indented, defaultSettings);
var upgradeLevels2 = new UpgradeLevel []
new(visual, default, default),
new(visual, default, default),
var buildableData = new BuildableData(prefab, default, default, default, default, upgradeLevels2);
var json1 = JsonConvert.SerializeObject(buildableData, Formatting.Indented, defaultSettings);
JsonConvert.PopulateObject(json, buildableData, defaultSettings);
var json2 = JsonConvert.SerializeObject(buildableData, Formatting.Indented, defaultSettings);
Console.WriteLine("{0} before PopulateObject():", buildableData);
Console.WriteLine(json1);
Console.WriteLine("Input JSON:");
Console.WriteLine("{0} after PopulateObject():", buildableData);
Console.WriteLine(json2);
Assert.That(converter.CanConvert(typeof(UpgradeLevel [])));
Assert.AreEqual(json, json);
Assert.AreEqual(buildableData.GetData().prefab, prefab, "buildableData.GetData().prefab, == prefab");
Assert.That(buildableData.GetData().upgradeLevels.All(l => l.GetData().visual == visual), "All(l => l.GetData().visual == visual)");
static string GetJson() =>
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: ");