Browse Source

Breaking change: Added a new JsonContent class that replaces JsonList and JsonDirectory.
Updated to System.Text.Json 5.0.0

Vicente Penades 5 years ago
parent
commit
31f48ddf44

+ 0 - 169
src/SharpGLTF.Core/IO/JsonCollections.cs

@@ -1,169 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.Json;
-
-using JSONELEMENT = System.Text.Json.JsonElement;
-
-namespace SharpGLTF.IO
-{
-    static class JsonValue
-    {
-        public static bool IsJsonSerializable(Object value, out Object invalidValue)
-        {
-            invalidValue = null;
-
-            if (value == null) return false;
-
-            if (value is IConvertible cvt)
-            {
-                var t = cvt.GetTypeCode();
-                if (t == TypeCode.Empty) { invalidValue = value; return false; }
-                if (t == TypeCode.DBNull) { invalidValue = value; return false; }
-                if (t == TypeCode.Object) { invalidValue = value; return false; }
-                if (t == TypeCode.DateTime) { invalidValue = value; return false; }
-                return true;
-            }
-
-            if (value is JsonList list)
-            {
-                foreach (var item in list)
-                {
-                    if (!IsJsonSerializable(item, out invalidValue)) return false;
-                }
-                return true;
-            }
-
-            if (value is JsonDictionary dict)
-            {
-                foreach (var item in dict.Values)
-                {
-                    if (!IsJsonSerializable(item, out invalidValue)) return false;
-                }
-                return true;
-            }
-
-            invalidValue = value;
-            return false;
-        }
-
-        public static bool IsJsonSerializable(Object value) { return IsJsonSerializable(value, out _); }
-
-        public static string SerializeToJson(Object value, System.Text.Json.JsonSerializerOptions options)
-        {
-            if (!IsJsonSerializable(value, out Object invalidValue)) throw new ArgumentException($"Found {invalidValue}, Expected Values, JsonList and JsonDictionary types allowed.", nameof(value));
-
-            if (options == null)
-            {
-                options = new System.Text.Json.JsonSerializerOptions
-                {
-                    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
-                    IgnoreNullValues = true,
-                    WriteIndented = true
-                };
-            }
-
-            return System.Text.Json.JsonSerializer.Serialize(value, value.GetType(), options);
-        }
-
-        /// <summary>
-        /// Clones a json hierarchical object.
-        /// </summary>
-        /// <param name="value">An Iconvertible object, a List, or a Dictionary</param>
-        /// <returns>A cloned object</returns>
-        public static Object DeepClone(Object value)
-        {
-            if (value == null) throw new ArgumentNullException(nameof(value));
-
-            if (value is IConvertible cvt)
-            {
-                var t = cvt.GetTypeCode();
-                if (t == TypeCode.Empty) throw new ArgumentException($"Unexpected type {t}", nameof(value));
-                if (t == TypeCode.DBNull) throw new ArgumentException($"Unexpected type {t}", nameof(value));
-                if (t == TypeCode.Object) throw new ArgumentException($"Unexpected type {t}", nameof(value));
-                if (t == TypeCode.DateTime) throw new ArgumentException($"Unexpected type {t}", nameof(value));
-                return value;
-            }
-
-            if (value is IDictionary<string, Object> wadict) return new JsonDictionary(wadict);
-            if (value is IReadOnlyDictionary<string, Object> rodict) return new JsonDictionary(rodict);
-
-            if (value is IList<Object> walist) return new JsonList(walist);
-            if (value is IReadOnlyList<Object> rolist) return new JsonList(rolist);
-
-            throw new ArgumentException($"Unexpected type {value.GetType().Name}", nameof(value));
-        }
-
-        public static Object DeepParse(string json, JsonDocumentOptions options = default)
-        {
-            using (var doc = System.Text.Json.JsonDocument.Parse(json, options))
-            {
-                return DeepClone(doc);
-            }
-        }
-
-        public static Object DeepClone(System.Text.Json.JsonDocument doc)
-        {
-            return DeepClone(doc.RootElement);
-        }
-
-        public static Object DeepClone(JSONELEMENT element)
-        {
-            if (element.ValueKind == JsonValueKind.Null) return null;
-            if (element.ValueKind == JsonValueKind.False) return false;
-            if (element.ValueKind == JsonValueKind.True) return true;
-            if (element.ValueKind == JsonValueKind.String) return element.GetString();
-            if (element.ValueKind == JsonValueKind.Number) return element.GetRawText(); // use IConvertible interface when needed.
-            if (element.ValueKind == JsonValueKind.Array) return new JsonList(element);
-            if (element.ValueKind == JsonValueKind.Object) return new JsonDictionary(element);
-
-            throw new NotImplementedException();
-        }
-    }
-
-    public class JsonList : List<Object>
-    {
-        public JsonList() { }
-
-        internal JsonList(IEnumerable<Object> list)
-            : base(list) { }
-
-        internal JsonList(JSONELEMENT element)
-        {
-            if (element.ValueKind != JsonValueKind.Array) throw new ArgumentException("Must be JsonValueKind.Array", nameof(element));
-
-            foreach (var item in element.EnumerateArray())
-            {
-                var xitem = JsonValue.DeepClone(item);
-                this.Add(xitem);
-            }
-        }
-    }
-
-    public class JsonDictionary : Dictionary<String, Object>
-    {
-        public JsonDictionary() { }
-
-        internal JsonDictionary(IDictionary<String, Object> dict)
-            : base(dict) { }
-
-        internal JsonDictionary(IReadOnlyDictionary<String, Object> dict)
-        {
-            foreach (var kvp in dict)
-            {
-                this[kvp.Key] = kvp.Value;
-            }
-        }
-
-        internal JsonDictionary(JSONELEMENT element)
-        {
-            if (element.ValueKind != JsonValueKind.Object) throw new ArgumentException("Must be JsonValueKind.Object", nameof(element));
-
-            foreach (var item in element.EnumerateObject())
-            {
-                this[item.Name] = JsonValue.DeepClone(item.Value);
-            }
-        }
-    }
-}

+ 435 - 0
src/SharpGLTF.Core/IO/JsonContent.Impl.cs

@@ -0,0 +1,435 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+
+using CULTURE = System.Globalization.CultureInfo;
+using JSONELEMENT = System.Text.Json.JsonElement;
+using JSONOPTIONS = System.Text.Json.JsonSerializerOptions;
+using JSONPROPERTY = System.Collections.Generic.KeyValuePair<string, object>;
+
+namespace SharpGLTF.IO
+{
+    interface IJsonCollection : ICloneable
+    {
+        bool IsArray { get; }
+        bool IsObject { get; }
+        int Count { get; }
+    }
+
+    struct _JsonStaticUtils
+    {
+        public static string ToJson(Object obj, JSONOPTIONS options)
+        {
+            if (obj == null) return String.Empty;
+
+            if (options == null)
+            {
+                options = new JSONOPTIONS
+                {
+                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                    IgnoreNullValues = true,
+                    WriteIndented = true
+                };
+            }
+
+            return JsonSerializer.Serialize(obj, obj.GetType(), options);
+        }
+
+        /// <summary>
+        /// Serializes data trees into trees of <see cref="IConvertible"/>, <see cref="_JsonArray"/> and <see cref="_JsonObject"/>.
+        /// </summary>
+        /// <param name="value">Any <see cref="IConvertible"/> array, list, or dictionary.</param>
+        /// <returns>An <see cref="IConvertible"/>, <see cref="_JsonArray"/> or <see cref="_JsonObject"/>.</returns>
+        public static Object Serialize(Object value)
+        {
+            if (value == null) throw new ArgumentNullException(nameof(value));
+            if (value is IConvertible cvalue && IsJsonSerializable(cvalue)) return cvalue;
+            if (_JsonObject.TryCreate(value, out _JsonObject dict)) return dict;
+            if (_JsonArray.TryCreate(value, out _JsonArray array)) return array;
+
+            throw new ArgumentException($"Can't serialize {value.GetType().Name}", nameof(value));
+        }
+
+        public static Object Deserialize(Object obj, Type type, JSONOPTIONS options = null)
+        {
+            if (options == null)
+            {
+                options = new JSONOPTIONS
+                {
+                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                    IgnoreNullValues = true,
+                    WriteIndented = true
+                };
+            }
+
+            var json = ToJson(obj, options);
+
+            return JsonSerializer.Deserialize(json, type, options);
+        }
+
+        public static Object Deserialize(JSONELEMENT element)
+        {
+            if (element.ValueKind == JsonValueKind.Null) return null;
+            if (element.ValueKind == JsonValueKind.False) return false;
+            if (element.ValueKind == JsonValueKind.True) return true;
+            if (element.ValueKind == JsonValueKind.String) return element.GetString();
+            if (element.ValueKind == JsonValueKind.Number) return element.GetDouble();
+            if (element.ValueKind == JsonValueKind.Array) return _JsonArray.CreateFrom(element);
+            if (element.ValueKind == JsonValueKind.Object) return _JsonObject.CreateFrom(element);
+
+            throw new NotImplementedException();
+        }
+
+        public static Object GetNode(Object current, params IConvertible[] path)
+        {
+            foreach (var part in path)
+            {
+                if (part is int index && current is IReadOnlyList<Object> array) { current = array[index]; continue; }
+                if (part is string key && current is IReadOnlyDictionary<String, Object> dict) { current = dict[key]; continue; }
+                throw new ArgumentException("Invalid path", nameof(path));
+            }
+
+            return current;
+        }
+
+        public static T GetValue<T>(Object current, params IConvertible[] path)
+            where T : IConvertible
+        {
+            current = GetNode(current, path);
+
+            if (current is IConvertible value)
+            {
+                return (T)Convert.ChangeType(value, typeof(T), CULTURE.InvariantCulture);
+            }
+
+            throw new ArgumentException("Invalid path", nameof(path));
+        }
+
+        public static bool IsJsonSerializable(Object value) { return IsJsonSerializable(value, out _); }
+
+        public static bool IsJsonSerializable(Object value, out Object invalidValue)
+        {
+            invalidValue = null;
+
+            if (value == null) return false;
+
+            if (value is IConvertible cvt) return IsJsonSerializable(cvt);
+
+            if (value is IDictionary dict)
+            {
+                if (dict.Count == 0) { invalidValue = value; return false; }
+
+                foreach (DictionaryEntry entry in dict)
+                {
+                    if (!IsJsonSerializable(entry.Value, out invalidValue)) return false;
+                }
+
+                return true;
+            }
+
+            if (value is IReadOnlyDictionary<string, object> dictXY)
+            {
+                if (dictXY.Count == 0) { invalidValue = value; return false; }
+
+                foreach (var item in dictXY.Values)
+                {
+                    if (!IsJsonSerializable(item, out invalidValue)) return false;
+                }
+
+                return true;
+            }
+
+            if (value is IEnumerable array)
+            {
+                if (!array.Cast<Object>().Any()) { invalidValue = value; return false; }
+
+                foreach (var item in array)
+                {
+                    if (!IsJsonSerializable(item, out invalidValue)) return false;
+                }
+
+                return true;
+            }
+
+            invalidValue = value;
+
+            return false;
+        }
+
+        private static bool IsJsonSerializable(IConvertible cvt)
+        {
+            switch (cvt.GetTypeCode())
+            {
+                case TypeCode.Empty:
+                case TypeCode.DBNull:
+                case TypeCode.Object:
+                case TypeCode.DateTime:
+                    return false;
+            }
+
+            if (cvt is Single scvt) return !Single.IsNaN(scvt) && !Single.IsInfinity(scvt);
+            if (cvt is Double dcvt) return !Double.IsNaN(dcvt) && !Double.IsInfinity(dcvt);
+
+            return true;
+        }
+    }
+
+    /// <summary>
+    /// Represents an inmutable Json Array.
+    /// </summary>
+    readonly struct _JsonArray : IReadOnlyList<object>, IJsonCollection, IList
+    {
+        #region constructor
+
+        public static bool TryCreate(Object value, out _JsonArray obj)
+        {
+            if (value is IConvertible _) { obj = default; return false; }
+            if (value is IDictionary _) { obj = default; return false; }
+            if (value is IEnumerable collection) { obj = _From(collection); return true; }
+
+            obj = default;
+            return false;
+        }
+
+        public static _JsonArray CreateFrom(JSONELEMENT array)
+        {
+            if (array.ValueKind != JsonValueKind.Array) throw new ArgumentException("Must be JsonValueKind.Array", nameof(array));
+
+            Object convert(JsonElement element)
+            {
+                return _JsonStaticUtils.Deserialize(element);
+            }
+
+            using (var entries = array.EnumerateArray())
+            {
+                return _From(entries.Select(convert));
+            }
+        }
+
+        private static _JsonArray _From(IEnumerable collection) { return new _JsonArray(collection); }
+
+        private _JsonArray(IEnumerable collection)
+        {
+            // 1st pass: determine element type and collection size
+
+            Type elementType = null;
+
+            int count = 0;
+
+            foreach (var item in collection.Cast<Object>())
+            {
+                if (item == null) throw new ArgumentException($"{nameof(collection)}[{count}] is null", nameof(collection));
+
+                ++count;
+
+                if (elementType == null)
+                {
+                    if (item is IConvertible) elementType = typeof(IConvertible);
+                    else elementType = item.GetType();
+                    continue;
+                }
+
+                if (!elementType.IsAssignableFrom(item.GetType())) throw new ArgumentException($"{nameof(collection)}[{count}] is invalid type.", nameof(collection));
+            }
+
+            if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
+            {
+                elementType = typeof(IDictionary);
+            }
+
+            int contentType = 0;
+            if (elementType == typeof(IConvertible)) contentType = 1;
+            if (contentType == 0 && elementType == typeof(IDictionary)) contentType = 3;
+            if (contentType == 0 && elementType.IsAssignableFrom(typeof(IEnumerable))) contentType = 2;
+
+            switch (contentType)
+            {
+                case 1: _Array = Array.CreateInstance(typeof(IConvertible), count); break;
+                case 2: _Array = Array.CreateInstance(typeof(_JsonArray), count); break;
+                case 3: _Array = Array.CreateInstance(typeof(_JsonObject), count); break;
+                default: throw new NotImplementedException();
+            }
+
+            // 2nd pass: convert and assign items.
+
+            int idx = 0;
+            foreach (var item in collection) _Array.SetValue(_JsonStaticUtils.Serialize(item), idx++);
+        }
+
+        public object Clone() { return _From(this); }
+
+        #endregion
+
+        #region data
+
+        private readonly Array _Array;
+
+        #endregion
+
+        #region properties
+
+        public Object this[int index] => _Array.GetValue(index);
+
+        object IList.this[int index]
+        {
+            get => _Array.GetValue(index);
+            set => throw new NotSupportedException();
+        }
+
+        public bool IsArray => true;
+        public bool IsObject => false;
+        public int Count => _Array.Length;
+        bool IList.IsFixedSize => true;
+        bool IList.IsReadOnly => true;
+        bool ICollection.IsSynchronized => false;
+        object ICollection.SyncRoot => null;
+
+        #endregion
+
+        #region API
+        public IEnumerator<Object> GetEnumerator() { return _Array.Cast<object>().GetEnumerator(); }
+        IEnumerator IEnumerable.GetEnumerator() { return _Array.GetEnumerator(); }
+        public bool Contains(object value) { return IndexOf(value) >= 0; }
+        public int IndexOf(object value) { return Array.IndexOf(_Array, value); }
+        public void CopyTo(Array array, int index) { _Array.CopyTo(array, index); }
+        void IList.Clear() { throw new NotSupportedException(); }
+        void IList.Insert(int index, object value) { throw new NotSupportedException(); }
+        void IList.Remove(object value) { throw new NotSupportedException(); }
+        void IList.RemoveAt(int index) { throw new NotSupportedException(); }
+        int IList.Add(object value) { throw new NotSupportedException(); }
+
+        #endregion
+    }
+
+    /// <summary>
+    /// Represents an inmutable Json Object.
+    /// </summary>
+    /// <remarks>
+    /// Supported by converter <see href="https://github.com/dotnet/runtime/blob/76904319b41a1dd0823daaaaae6e56769ed19ed3/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs"/>
+    /// </remarks>
+    readonly struct _JsonObject : IReadOnlyDictionary<string, object>, IDictionary, IJsonCollection
+    {
+        #region constructor
+
+        public static bool TryCreate(Object value, out _JsonObject obj)
+        {
+            if (value is IConvertible _) { obj = default; return false; }
+            if (value is IDictionary dict0) { obj = new _JsonObject(_Enumerate(dict0)); return true; }
+            if (value is IReadOnlyDictionary<string, Object> dict1) { obj = new _JsonObject(_Enumerate(dict1)); return true; }
+
+            obj = default;
+            return false;
+        }
+
+        public static _JsonObject CreateFrom(JSONELEMENT dict)
+        {
+            if (dict.ValueKind != JsonValueKind.Object) throw new ArgumentException("Must be JsonValueKind.Object", nameof(dict));
+
+            JSONPROPERTY convert(JsonProperty property)
+            {
+                var value = _JsonStaticUtils.Deserialize(property.Value);
+                return new JSONPROPERTY(property.Name, value);
+            }
+
+            using (var entries = dict.EnumerateObject())
+            {
+                return new _JsonObject(dict.EnumerateObject().Select(convert));
+            }
+        }
+
+        private static IEnumerable<JSONPROPERTY> _Enumerate(IDictionary dict)
+        {
+            foreach (DictionaryEntry entry in dict)
+            {
+                yield return new JSONPROPERTY(_GetKey(entry.Key), _GetValue(entry.Value));
+            }
+        }
+
+        private static IEnumerable<JSONPROPERTY> _Enumerate<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>> dict)
+        {
+            foreach (var entry in dict)
+            {
+                yield return new JSONPROPERTY(_GetKey(entry.Key), _GetValue(entry.Value));
+            }
+        }
+
+        private static string _GetKey(Object key)
+        {
+            if (key is string skey) return skey;
+            if (key is IConvertible ckey) return ckey.ToString(CULTURE.InvariantCulture);
+            return key.ToString();
+        }
+
+        private static Object _GetValue(Object value)
+        {
+            return _JsonStaticUtils.Serialize(value);
+        }
+
+        public object Clone()
+        {
+            return new _JsonObject(_Dictionary);
+        }
+
+        private _JsonObject(IEnumerable<JSONPROPERTY> items)
+        {
+            bool _filterEmptyCollections(JSONPROPERTY p)
+            {
+                if (p.Value is IConvertible) return true;
+                if (p.Value is IJsonCollection c) return c.Count > 0;
+                throw new ArgumentException($"{p.GetType().Name} not supported.", nameof(items));
+            }
+
+            items = items.Where(_filterEmptyCollections);
+
+            _Dictionary = new Dictionary<string, object>();
+
+            foreach (var item in items) _Dictionary.Add(item.Key, item.Value);
+        }
+
+        #endregion
+
+        #region data
+
+        private readonly Dictionary<String, Object> _Dictionary;
+
+        #endregion
+
+        #region properties
+        public object this[string key] => _Dictionary[key];
+        public IEnumerable<string> Keys => _Dictionary.Keys;
+        ICollection IDictionary.Keys => _Dictionary.Keys;
+        public IEnumerable<object> Values => _Dictionary.Values;
+        ICollection IDictionary.Values => _Dictionary.Values;
+        public int Count => _Dictionary.Count;
+        public bool IsArray => false;
+        public bool IsObject => true;
+        public bool IsFixedSize => true;
+        public bool IsReadOnly => true;
+        public bool IsSynchronized => false;
+        public object SyncRoot => null;
+        public object this[object key]
+        {
+            get => this[(string)key];
+            set => throw new NotSupportedException();
+        }
+
+        #endregion
+
+        #region API
+
+        bool IDictionary.Contains(object key) { return ContainsKey((string)key); }
+        public bool ContainsKey(string key) { return _Dictionary.ContainsKey(key); }
+        public bool TryGetValue(string key, out object value) { return _Dictionary.TryGetValue(key, out value); }
+        IEnumerator IEnumerable.GetEnumerator() { return _Dictionary.GetEnumerator(); }
+        public IEnumerator<JSONPROPERTY> GetEnumerator() { return _Dictionary.AsEnumerable().GetEnumerator(); }
+        IDictionaryEnumerator IDictionary.GetEnumerator() { return ((IDictionary)_Dictionary).GetEnumerator(); }
+        void IDictionary.Add(object key, object value) { throw new NotSupportedException(); }
+        void IDictionary.Clear() { throw new NotSupportedException(); }
+        void IDictionary.Remove(object key) { throw new NotSupportedException(); }
+        void ICollection.CopyTo(Array array, int index) { throw new NotImplementedException(); }
+
+        #endregion
+    }
+}

+ 170 - 0
src/SharpGLTF.Core/IO/JsonContent.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+
+using JSONOPTIONS = System.Text.Json.JsonSerializerOptions;
+
+namespace SharpGLTF.IO
+{
+    /// <summary>
+    /// Represents an inmutable json object stored in memory.
+    /// </summary>
+    /// <remarks>
+    /// Valid values can be:
+    /// - <see cref="IConvertible"/> for literal values.
+    /// - <see cref="IReadOnlyList{Object}"/> for arrays.
+    /// - <see cref="IReadOnlyDictionary{String, Object}"/> for objects.
+    /// </remarks>
+    [System.ComponentModel.ImmutableObject(true)]
+    public readonly struct JsonContent
+    {
+        #region constructors
+
+        public static implicit operator JsonContent(Boolean value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(String value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Int32 value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Int64 value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Single value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Double value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Object[] value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(List<Object> value) { return new JsonContent(value); }
+
+        public static implicit operator JsonContent(Dictionary<String, Object> value) { return new JsonContent(value); }
+
+        public static JsonContent CreateFrom(IConvertible value) { return new JsonContent(value); }
+        public static JsonContent CreateFrom(IList value) { return new JsonContent(value); }
+        public static JsonContent CreateFrom(IDictionary value) { return new JsonContent(value); }
+
+        internal static JsonContent _Wrap(Object value) { return new JsonContent(value); }
+
+        public JsonContent DeepClone() { return new JsonContent(_Content);  }
+
+        private JsonContent(Object value)
+        {
+            _Content = value == null ? null : _JsonStaticUtils.Serialize(value);
+            if (_Content is IJsonCollection collection && collection.Count == 0)
+                _Content = null;
+        }
+
+        #endregion
+
+        #region data
+
+        /// <summary>
+        /// The dynamic json structure, where it can be any of this:
+        /// - A <see cref="IConvertible"/> object.
+        /// - A non empty <see cref="IReadOnlyList{Object}"/> object.
+        /// - A non empty <see cref="IReadOnlyDictionary{String, Object}"/> object.
+        /// </summary>
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private readonly Object _Content;
+
+        // It is tempting to add Equality support, but it's problematic because these reasons:
+        // - It's not clear how to compare in-memory floating point values against deserialized string values.
+        // - Serialization roundtrip is not well supported in older NetFramework versions; this is specially
+        // apparent when using System.Text.JSon in NetCore and Net471, where NetCore is roundtrip safe, and
+        // NetFramework is not.
+
+        #endregion
+
+        #region properties
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Collapsed)]
+        public Object Content => _Content;
+
+        #endregion
+
+        #region serialization
+
+        /// <summary>
+        /// Converts the value of a specified type into a <see cref="JsonContent"/> using <see cref="JsonSerializer"/>.
+        /// </summary>
+        /// <param name="value">The value to convert.</param>
+        /// <param name="options">Options to control the conversion behavior.</param>
+        /// <returns>A <see cref="JsonContent"/> object.</returns>
+        public static JsonContent Serialize(Object value, JSONOPTIONS options = null)
+        {
+            if (value == null) return default;
+
+            if (options == null)
+            {
+                options = new JSONOPTIONS
+                {
+                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                    IgnoreNullValues = true,
+                    WriteIndented = true
+                };
+            }
+
+            var json = JsonSerializer.Serialize(value, value.GetType(), options);
+
+            return Parse(json);
+        }
+
+        /// <summary>
+        /// Parses a json text an converts it to a <see cref="JsonContent"/>
+        /// </summary>
+        /// <param name="jsonContent">The json text content.</param>
+        /// <param name="options">Parser options.</param>
+        /// <returns>A <see cref="JsonContent"/> object</returns>
+        public static JsonContent Parse(string jsonContent, JsonDocumentOptions options = default)
+        {
+            using (var doc = JsonDocument.Parse(jsonContent, options))
+            {
+                return Parse(doc);
+            }
+        }
+
+        public static JsonContent Parse(JsonDocument root)
+        {
+            return root == null ? default : new JsonContent(_JsonStaticUtils.Deserialize(root.RootElement));
+        }
+
+        public string ToJson(JSONOPTIONS options = null)
+        {
+            return _JsonStaticUtils.ToJson(_Content, options);
+        }
+
+        public Object Deserialize(Type type, JSONOPTIONS options = null)
+        {
+            return _JsonStaticUtils.Deserialize(_Content, type, options);
+        }
+
+        #endregion
+
+        #region static API
+        public static bool IsJsonSerializable(Object value) { return IsJsonSerializable(value, out _); }
+
+        public static bool IsJsonSerializable(Object value, out Object invalidValue)
+        {
+            return _JsonStaticUtils.IsJsonSerializable(value, out invalidValue);
+        }
+
+        #endregion
+
+        #region API
+
+        public JsonContent GetNode(params IConvertible[] path)
+        {
+            var value = _JsonStaticUtils.GetNode(this._Content, path);
+            return new JsonContent(value);
+        }
+
+        public T GetValue<T>(params IConvertible[] path)
+            where T : IConvertible
+        {
+            return _JsonStaticUtils.GetValue<T>(this._Content, path);
+        }
+
+        #endregion
+    }
+}

+ 15 - 2
src/SharpGLTF.Core/IO/JsonSerializable.cs

@@ -11,6 +11,10 @@ using JSONTOKEN = System.Text.Json.JsonTokenType;
 
 namespace SharpGLTF.IO
 {
+    /// <summary>
+    /// Represents the base class of a serializable glTF schema2 object.
+    /// Inherited by <see cref="Schema2.ExtraProperties"/>.
+    /// </summary>
     public abstract class JsonSerializable
     {
         #region validation
@@ -225,12 +229,21 @@ namespace SharpGLTF.IO
             Guard.NotNull(writer, nameof(writer));
             Guard.NotNull(value, nameof(value));
 
+            if (_IsNullOrEmpty(value)) return;
+
             if (writer.TryWriteProperty(name, value)) return;
 
             writer.WritePropertyName(name);
             _SerializeValue(writer, value);
         }
 
+        private static bool _IsNullOrEmpty(Object value)
+        {
+            if (value == null) return true;
+            if (value is System.Collections.ICollection c && c.Count == 0) return true;
+            return false;
+        }
+
         private static void _SerializeValue(Utf8JsonWriter writer, Object value)
         {
             Guard.NotNull(writer, nameof(writer));
@@ -324,7 +337,7 @@ namespace SharpGLTF.IO
 
             if (reader.TokenType == JSONTOKEN.StartArray)
             {
-                var list = new JsonList();
+                var list = new List<Object>();
 
                 while (reader.Read() && reader.TokenType != JSONTOKEN.EndArray)
                 {
@@ -336,7 +349,7 @@ namespace SharpGLTF.IO
 
             if (reader.TokenType == JSONTOKEN.StartObject)
             {
-                var dict = new JsonDictionary();
+                var dict = new Dictionary<String, Object>();
 
                 while (reader.Read() && reader.TokenType != JSONTOKEN.EndObject)
                 {

+ 27 - 2
src/SharpGLTF.Core/IO/Unknown.cs

@@ -6,18 +6,41 @@ using System.Text.Json;
 
 namespace SharpGLTF.IO
 {
+    /// <summary>
+    /// Represents a node with an unknown type in a glTF Schema.
+    /// </summary>
+    /// <remarks>
+    /// When parsing a glTF json, the json object types are usually known at runtime,
+    /// So the types are instantiated as the json is being parsed. But it can happen
+    /// that we can find a json object for which the type is not known at runtime.
+    /// This usually happens with unknown extensions, which are stored using this
+    /// object.
+    /// </remarks>
     [System.Diagnostics.DebuggerDisplay("Unknown {_Name}")]
     class UnknownNode : JsonSerializable
     {
+        #region lifecycle
         public UnknownNode(string name) { this._Name = name; }
 
+        #endregion
+
+        #region data
+
         private readonly string _Name;
 
-        private readonly JsonDictionary _Properties = new JsonDictionary();
+        private readonly Dictionary<String, Object> _Properties = new Dictionary<String, Object>();
+
+        #endregion
+
+        #region properties
 
         public string Name => _Name;
 
-        public JsonDictionary Properties => _Properties;
+        public IReadOnlyDictionary<String, Object> Properties => _Properties;
+
+        #endregion
+
+        #region API
 
         protected override void DeserializeProperty(string property, ref Utf8JsonReader reader)
         {
@@ -32,5 +55,7 @@ namespace SharpGLTF.IO
                 SerializeProperty(writer, kvp.Key, kvp.Value);
             }
         }
+
+        #endregion
     }
 }

+ 1 - 1
src/SharpGLTF.Core/Memory/MemoryImage.cs

@@ -476,7 +476,7 @@ namespace SharpGLTF.Memory
             // header must be valid
             Guard.IsTrue(header.IsValidHeader, paramName + ".Header");
 
-            // pixelWidth and pixelHeight MUST be multiples of 4. 
+            // pixelWidth and pixelHeight MUST be multiples of 4.
             Guard.MustBePositiveAndMultipleOf((int)header.pixelWidth, 4, $"{paramName}.{nameof(pixelWidth)}");
             Guard.MustBePositiveAndMultipleOf((int)header.pixelHeight, 4, $"{paramName}.{nameof(pixelHeight)}");
 

+ 0 - 22
src/SharpGLTF.Core/Schema2/gltf.Asset.cs

@@ -65,28 +65,6 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
-        #region API
-
-        private string _GetExtraInfo(string key)
-        {
-            if (this.Extras is IReadOnlyDictionary<string, Object> dict)
-            {
-                return dict.TryGetValue(key, out Object val) ? val as String : null;
-            }
-            else
-            {
-                return null;
-            }
-        }
-
-        private void _SetExtraInfo(string key, string val)
-        {
-            throw new NotImplementedException();
-            // if (this.Extras == null) this.Extras = new Dictionary<string, Object>();
-        }
-
-        #endregion
-
         #region Validation
 
         protected override void OnValidateReferences(Validation.ValidationContext validate)

+ 29 - 43
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -11,13 +11,19 @@ using JsonToken = System.Text.Json.JsonTokenType;
 
 namespace SharpGLTF.Schema2
 {
+    /// <summary>
+    /// Represents the base class for all glTF 2 Schema objects.
+    /// </summary>
+    /// <remarks>
+    /// Defines the <see cref="Extras"/> property for every glTF object.
+    /// </remarks>
     public abstract class ExtraProperties : JsonSerializable
     {
         #region data
 
         private readonly List<JsonSerializable> _extensions = new List<JsonSerializable>();
 
-        private Object _extras;
+        private IO.JsonContent _extras;
 
         #endregion
 
@@ -29,9 +35,16 @@ namespace SharpGLTF.Schema2
         public IReadOnlyCollection<JsonSerializable> Extensions => _extensions;
 
         /// <summary>
-        /// Gets the extras value, where the value can be either an intrinsic type <see cref="TypeCode"/> , a <see cref="JsonList"/> or a <see cref="JsonDictionary"/>
+        /// Gets the extras of this instance, where the value can be
+        /// an <see cref="IConvertible"/>,
+        /// a <see cref="IReadOnlyList{Object}"/> or
+        /// a <see cref="IReadOnlyDictionary{String, Object}"/>.
         /// </summary>
-        public Object Extras => _extras;
+        public IO.JsonContent Extras
+        {
+            get => _extras;
+            set => _extras = value.DeepClone();
+        }
 
         #endregion
 
@@ -90,34 +103,6 @@ namespace SharpGLTF.Schema2
             }
         }
 
-        /// <summary>
-        /// Gets the Extras property as a <see cref="JsonDictionary"/>
-        /// </summary>
-        /// <param name="overwrite">true if the current value is to be replaced by a <see cref="JsonDictionary"/> instance.</param>
-        /// <returns>A <see cref="JsonDictionary"/> instance or null.</returns>
-        public JsonDictionary TryUseExtrasAsDictionary(bool overwrite)
-        {
-            if (this._extras is JsonDictionary dict) return dict;
-
-            if (overwrite) this._extras = new JsonDictionary();
-
-            return this._extras as JsonDictionary;
-        }
-
-        /// <summary>
-        /// Gets the Extras property as a <see cref="JsonList"/>
-        /// </summary>
-        /// <param name="overwrite">true if the current value is to be replaced by a <see cref="JsonList"/> instance.</param>
-        /// <returns>A <see cref="JsonDictionary"/> instance or null.</returns>
-        public JsonList TryUseExtrasAsList(bool overwrite)
-        {
-            if (this._extras is JsonList list) return list;
-
-            if (overwrite) this._extras = new JsonList();
-
-            return this._extras as JsonList;
-        }
-
         #endregion
 
         #region validation
@@ -133,7 +118,7 @@ namespace SharpGLTF.Schema2
 
             foreach (var ext in this.Extensions) ext.ValidateReferences(validate);
 
-            if (this._extras is JsonSerializable js) js.ValidateReferences(validate);
+            if (this._extras.Content is JsonSerializable js) js.ValidateReferences(validate);
         }
 
         protected override void OnValidateContent(Validation.ValidationContext validate)
@@ -145,9 +130,9 @@ namespace SharpGLTF.Schema2
                 lc.ValidateContent(validate);
             }
 
-            if (this._extras is JsonSerializable js) js.ValidateContent(validate);
+            if (this._extras.Content is JsonSerializable js) js.ValidateContent(validate);
 
-            if (this._extras != null) validate.IsJsonSerializable("Extras", this._extras);
+            if (this._extras.Content != null) validate.IsJsonSerializable("Extras", this._extras.Content);
         }
 
         #endregion
@@ -166,16 +151,12 @@ namespace SharpGLTF.Schema2
                 SerializeProperty(writer, "extensions", dict);
             }
 
-            if (_extras == null) return;
-
             // todo, only write _extras if it's a known serializable type.
+            var content = _extras.Content;
+            if (content == null) return;
+            if (!IO.JsonContent.IsJsonSerializable(content)) return;
 
-            if (!(_extras is String) && _extras is System.Collections.IEnumerable collection)
-            {
-                if (!collection.Cast<Object>().Any()) return;
-            }
-
-            SerializeProperty(writer, "extras", _extras);
+            SerializeProperty(writer, "extras", content);
         }
 
         private static IReadOnlyDictionary<string, JsonSerializable> _ToDictionary(JsonSerializable context, IEnumerable<JsonSerializable> serializables)
@@ -211,7 +192,12 @@ namespace SharpGLTF.Schema2
             {
                 case "extensions": _DeserializeExtensions(this, ref reader, _extensions); break;
 
-                case "extras": _extras = DeserializeUnknownObject(ref reader); break;
+                case "extras":
+                    {
+                        var content = DeserializeUnknownObject(ref reader);
+                        _extras = JsonContent._Wrap(content);
+                        break;
+                    }
 
                 default: reader.Skip(); break;
             }

+ 6 - 1
src/SharpGLTF.Core/SharpGLTF.Core.csproj

@@ -24,13 +24,18 @@
   </ItemGroup>  
   
   <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
-    <PackageReference Include="System.Text.Json" Version="4.7.2" />
+    <PackageReference Include="System.Text.Json" Version="5.0.0" />
   </ItemGroup>
 
   <ItemGroup>
     <None Include="Schema2\Generated\*.cs">
       <ExcludeFromStyleCop>true</ExcludeFromStyleCop>
     </None>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1" />
+    <PackageReference Update="Microsoft.CodeQuality.Analyzers" Version="3.3.1" />
   </ItemGroup>  
 
 </Project>

+ 1 - 1
src/SharpGLTF.Core/Validation/ValidationContext.Guards.cs

@@ -144,7 +144,7 @@ namespace SharpGLTF.Validation
 
         public OUTTYPE IsJsonSerializable(PARAMNAME parameterName, Object value)
         {
-            if (!IO.JsonValue.IsJsonSerializable(value)) _SchemaThrow(parameterName, "cannot be serialized to Json");
+            if (!IO.JsonContent.IsJsonSerializable(value, out Object invalidValue)) _SchemaThrow(parameterName, $"{invalidValue?.GetType()?.Name} cannot be serialized to Json");
             return this;
         }
 

+ 3 - 0
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -68,6 +68,7 @@ namespace SharpGLTF.Geometry
             Guard.NotNull(other, nameof(other));
 
             this.Name = other.Name;
+            this.Extras = other.Extras.DeepClone();
             this._VertexPreprocessor = other._VertexPreprocessor;
 
             foreach (var kvp in other._Primitives)
@@ -109,6 +110,8 @@ namespace SharpGLTF.Geometry
 
         public string Name { get; set; }
 
+        public IO.JsonContent Extras { get; set; }
+
         public VertexPreprocessor<TvG, TvM, TvS> VertexPreprocessor
         {
             get => _VertexPreprocessor;

+ 2 - 0
src/SharpGLTF.Toolkit/Geometry/MeshBuilderToolkit.cs

@@ -12,6 +12,8 @@ namespace SharpGLTF.Geometry
     {
         string Name { get; set; }
 
+        IO.JsonContent Extras { get; set; }
+
         IEnumerable<TMaterial> Materials { get; }
 
         IReadOnlyCollection<IPrimitiveReader<TMaterial>> Primitives { get; }

+ 9 - 2
src/SharpGLTF.Toolkit/Geometry/PackedMeshBuilder.cs

@@ -42,7 +42,7 @@ namespace SharpGLTF.Geometry
 
             foreach (var srcMesh in meshBuilders)
             {
-                var dstMesh = new PackedMeshBuilder<TMaterial>(srcMesh.Name);
+                var dstMesh = new PackedMeshBuilder<TMaterial>(srcMesh.Name, srcMesh.Extras);
 
                 foreach (var srcPrim in srcMesh.Primitives)
                 {
@@ -64,7 +64,11 @@ namespace SharpGLTF.Geometry
             }
         }
 
-        private PackedMeshBuilder(string name) { _MeshName = name; }
+        private PackedMeshBuilder(string name, IO.JsonContent extras)
+        {
+            _MeshName = name;
+            _MeshExtras = extras;
+        }
 
         #endregion
 
@@ -72,6 +76,8 @@ namespace SharpGLTF.Geometry
 
         private readonly string _MeshName;
 
+        private readonly IO.JsonContent _MeshExtras;
+
         private readonly List<PackedPrimitiveBuilder<TMaterial>> _Primitives = new List<PackedPrimitiveBuilder<TMaterial>>();
 
         #endregion
@@ -91,6 +97,7 @@ namespace SharpGLTF.Geometry
             if (_Primitives.Count == 0) return null;
 
             var dstMesh = root.CreateMesh(_MeshName);
+            dstMesh.Extras = _MeshExtras.DeepClone();
 
             foreach (var p in _Primitives)
             {

+ 15 - 1
src/SharpGLTF.Toolkit/Scenes/NodeBuilder.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.Design;
 using System.Linq;
 using System.Numerics;
 using System.Text;
@@ -76,6 +77,8 @@ namespace SharpGLTF.Scenes
             clone._Rotation = this._Rotation?.Clone();
             clone._Translation = this._Translation?.Clone();
 
+            clone.Extras = this.Extras.DeepClone();
+
             foreach (var c in _Children)
             {
                 clone.AddNode(c.DeepClone(nodeMap));
@@ -99,10 +102,15 @@ namespace SharpGLTF.Scenes
 
         #endregion
 
-        #region properties - hierarchy
+        #region properties
 
         public String Name { get; set; }
 
+        public IO.JsonContent Extras { get; set; }
+
+        #endregion
+
+        #region properties - hierarchy
         public NodeBuilder Parent => _Parent;
 
         public NodeBuilder Root => _Parent == null ? this : _Parent.Root;
@@ -492,6 +500,12 @@ namespace SharpGLTF.Scenes
             return this;
         }
 
+        public NodeBuilder WithExtras(IO.JsonContent content)
+        {
+            Extras = content;
+            return this;
+        }
+
         #endregion
     }
 }

+ 6 - 0
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.Schema2.cs

@@ -116,6 +116,8 @@ namespace SharpGLTF.Scenes
                 dstNode.LocalMatrix = srcNode.LocalMatrix;
             }
 
+            dstNode.Extras = srcNode.Extras.DeepClone();
+
             foreach (var c in srcNode.VisualChildren) CreateArmature(dstNode.CreateNode, c);
         }
 
@@ -216,6 +218,7 @@ namespace SharpGLTF.Scenes
             var dstScene = dstModel.UseScene(0);
 
             dstScene.Name = this.Name;
+            dstScene.Extras = this.Extras.DeepClone();
 
             context.AddScene(dstScene, this);
 
@@ -252,6 +255,7 @@ namespace SharpGLTF.Scenes
             var dstScene = new SceneBuilder();
 
             dstScene.Name = srcScene.Name;
+            dstScene.Extras = srcScene.Extras.DeepClone();
 
             // process mesh instances
             var srcMeshInstances = Node.Flatten(srcScene)
@@ -355,6 +359,8 @@ namespace SharpGLTF.Scenes
             Guard.NotNull(dstNode, nameof(dstNode));
 
             dstNode.Name = srcNode.Name;
+            dstNode.Extras = srcNode.Extras.DeepClone();
+
             dstNode.LocalTransform = srcNode.LocalTransform;
 
             _CopyTransformAnimation(dstNode, srcNode);

+ 2 - 0
src/SharpGLTF.Toolkit/Scenes/SceneBuilder.cs

@@ -79,6 +79,8 @@ namespace SharpGLTF.Scenes
             set => _Name = value;
         }
 
+        public IO.JsonContent Extras { get; set; }
+
         [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
         public IReadOnlyList<InstanceBuilder> Instances => _Instances;
 

+ 1 - 0
src/SharpGLTF.Toolkit/Schema2/MeshExtensions.cs

@@ -612,6 +612,7 @@ namespace SharpGLTF.Schema2
 
             var dstMesh = MeshBuilderToolkit.CreateMeshBuilderFromVertexAttributes<Materials.MaterialBuilder>(vertexAttributes);
             dstMesh.Name = srcMesh.Name;
+            dstMesh.Extras = srcMesh.Extras.DeepClone();
 
             Materials.MaterialBuilder defMat = null;
 

+ 5 - 0
src/SharpGLTF.Toolkit/SharpGLTF.Toolkit.csproj

@@ -20,6 +20,11 @@
 
   <ItemGroup>
     <ProjectReference Include="..\SharpGLTF.Core\SharpGLTF.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Update="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1" />
+    <PackageReference Update="Microsoft.CodeQuality.Analyzers" Version="3.3.1" />
   </ItemGroup>  
   
 </Project>

+ 1 - 1
tests/SharpGLTF.DownloadTestFiles/SharpGLTF.DownloadTestFiles.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.2</TargetFramework>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
     <RootNamespace>SharpGLTF</RootNamespace>
   </PropertyGroup>
 

+ 155 - 0
tests/SharpGLTF.Tests/IO/JsonContentTests.cs

@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.IO
+{
+    class _TestStructure
+    {
+        public string Author { get; set; }
+        public int Integer1 { get; set; }
+        public bool Bool1 { get; set; }
+        public float Single1 { get; set; }
+        public float Single2 { get; set; }
+        public float Single3 { get; set; }
+
+        // public float SinglePI { get; set; } // Fails on .Net Framework 471
+
+        public double Double1 { get; set; }
+        public double Double2 { get; set; }
+        public double Double3 { get; set; }
+        public double DoublePI { get; set; }
+
+        public List<int> Array1 { get; set; }
+
+        public _TestStructure2 Dict1 { get; set; }
+        public _TestStructure3 Dict2 { get; set; }
+    }
+
+    struct _TestStructure2
+    {
+        public int A0 { get; set; }
+        public int A1 { get; set; }
+    }
+
+    class _TestStructure3
+    {
+        public int A { get; set; }
+        public string B { get; set; }
+
+        public int[] C { get; set; }
+
+        public _TestStructure2 D { get; set; }
+    }
+
+    [Category("Core.IO")]
+    public class JsonContentTests
+    {
+        // when serializing a JsonContent object, it's important to take into account floating point values roundtrips.
+        // it seems that prior NetCore3.1, System.Text.JSon was not roundtrip proven, so some values might have some
+        // error margin when they complete a roundtrip.
+
+        // On newer, NetCore System.Text.Json versions, it seems to use "G9" and "G17" text formatting are used.
+
+        // https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/            
+        // https://github.com/dotnet/runtime/blob/76904319b41a1dd0823daaaaae6e56769ed19ed3/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs#L101
+
+        // pull requests:
+        // https://github.com/dotnet/corefx/pull/40408
+        // https://github.com/dotnet/corefx/pull/38322
+        // https://github.com/dotnet/corefx/pull/32268
+
+        public static bool AreEqual(JsonContent a, JsonContent b)
+        {
+            if (Object.ReferenceEquals(a.Content, b.Content)) return true;
+            if (Object.ReferenceEquals(a.Content, null)) return false;
+            if (Object.ReferenceEquals(b.Content, null)) return false;
+
+            // A JsonContent ultimately represents a json block, so it seems fit to do the comparison that way.
+            // also, there's the problem of floating point json writing, that can slightly change between
+            // different frameworks.
+
+            var ajson = a.ToJson();
+            var bjson = b.ToJson();
+
+            return ajson == bjson;
+        }
+
+        [Test]
+        public void TestFloatingPointJsonRoundtrip()
+        {
+            float value = 1.1f; // serialized by system.text.json as 1.1000002f
+
+            var valueTxt = value.ToString("G9", System.Globalization.CultureInfo.InvariantCulture);
+
+            var dict = new Dictionary<string, Object>();            
+            dict["value"] = value;            
+
+            JsonContent a = dict;
+
+            // roundtrip to json
+            var json = a.ToJson();
+            TestContext.Write(json);
+            var b = IO.JsonContent.Parse(json);            
+
+            Assert.IsTrue(AreEqual(a, b));            
+        }
+
+        [Test]
+        public void CreateJsonContent()
+        {
+            var dict = new Dictionary<string, Object>();
+            dict["author"] = "me";
+            dict["integer1"] = 17;
+
+            dict["bool1"] = true;
+
+            dict["single1"] = 15.3f;
+            dict["single2"] = 1.1f;
+            dict["single3"] = -1.1f;
+            // dict["singlePI"] = (float)Math.PI; // Fails on .Net Framework 471
+
+            dict["double1"] = 15.3;
+            dict["double2"] = 1.1;
+            dict["double3"] = -1.1;
+            dict["doublePI"] = Math.PI;
+
+            dict["array1"] = new int[] { 1, 2, 3 };
+            dict["dict1"] = new Dictionary<string, int> { ["a0"] = 2, ["a1"] = 3 };
+            dict["dict2"] = new Dictionary<string, Object>
+            {
+                ["a"] = 16,
+                ["b"] = "delta",
+                ["c"] = new List<int>() { 4, 6, 7 },
+                ["d"] = new Dictionary<string, int> { ["a0"] = 1, ["a1"] = 2 }
+            };            
+
+            JsonContent a = dict;
+            
+            // roundtrip to json
+            var json = a.ToJson();
+            TestContext.Write(json);
+            var b = IO.JsonContent.Parse(json);            
+
+            // roundtrip to a runtime object
+            var x = a.Deserialize(typeof(_TestStructure));
+            var c = JsonContent.Serialize(x);
+
+            Assert.IsTrue(AreEqual(a, b));
+            Assert.IsTrue(AreEqual(a, c));
+
+            foreach (var dom in new[] { a, b, c})
+            {
+                Assert.AreEqual("me", dom.GetValue<string>("author"));
+                Assert.AreEqual(17, dom.GetValue<int>("integer1"));
+                Assert.AreEqual(15.3f, dom.GetValue<float>("single1"));
+                Assert.AreEqual(3, dom.GetValue<int>("array1", 2));
+                Assert.AreEqual(2, dom.GetValue<int>("dict2", "d", "a1"));
+            }            
+
+        }
+
+    }
+}

+ 23 - 23
tests/SharpGLTF.Tests/Schema2/Authoring/BasicSceneCreationTests.cs

@@ -1,7 +1,10 @@
-using System.Numerics;
+using System.Collections.Generic;
+using System.Numerics;
 
 using NUnit.Framework;
 
+using SharpGLTF.IO;
+
 namespace SharpGLTF.Schema2.Authoring
 {
     using VPOSNRM = Geometry.VertexBuilder<Geometry.VertexTypes.VertexPositionNormal,Geometry.VertexTypes.VertexEmpty,Geometry.VertexTypes.VertexEmpty>;
@@ -28,43 +31,40 @@ namespace SharpGLTF.Schema2.Authoring
             var root = ModelRoot.CreateModel();
             var scene = root.UseScene("Empty Scene");
 
-            var dict = root.TryUseExtrasAsDictionary(true);
+            var dict = new Dictionary<string, object>();
 
             dict["author"] = "me";
 
             dict["value1"] = 17;
-            dict["array1"] = new IO.JsonList { 1, 2, 3 };
-            dict["dict1"] = new IO.JsonDictionary
+            dict["array1"] = new List<int> { 1, 2, 3 };
+            dict["dict1"] = new Dictionary<string, object>
             {
                 ["A"] = 16,
                 ["B"] = "delta",
-                ["C"] = new IO.JsonList { 4, 6, 7 },
-                ["D"] = new IO.JsonDictionary { ["S"]= 1, ["T"] = 2 }
+                ["C"] = new List<int> { 4, 6, 7 },
+                ["D"] = new Dictionary<string, int> { ["S"]= 1, ["T"] = 2 }
             };
+            dict["dict2"] = new Dictionary<string, int> { ["2"] = 2, ["3"] = 3 };
 
-            var json = root.GetJSON(true);
+            JsonContent extras = dict;
+
+            root.Extras = extras;
 
+            var json = root.GetJSON(true);
             var bytes = root.WriteGLB();
             var rootBis = ModelRoot.ParseGLB(bytes);
 
-            var adict = root.TryUseExtrasAsDictionary(false);
-            var bdict = rootBis.TryUseExtrasAsDictionary(false);
-
-            CollectionAssert.AreEqual(adict, bdict);
+            var a = root.Extras;
+            var b = rootBis.Extras;
+            json = rootBis.Extras.ToJson();
+            var c = IO.JsonContent.Parse(json);
 
-            Assert.AreEqual(adict["author"], bdict["author"]);
-            Assert.AreEqual(adict["value1"], bdict["value1"]);
-            CollectionAssert.AreEqual
-                (
-                adict["array1"] as IO.JsonList,
-                bdict["array1"] as IO.JsonList
-                );
+            Assert.IsTrue(JsonContentTests.AreEqual(a,b));
+            Assert.IsTrue(JsonContentTests.AreEqual(a, extras));
+            Assert.IsTrue(JsonContentTests.AreEqual(b, extras));
+            Assert.IsTrue(JsonContentTests.AreEqual(c, extras));
 
-            CollectionAssert.AreEqual
-                (
-                adict["dict1"] as IO.JsonDictionary,
-                bdict["dict1"] as IO.JsonDictionary
-                );
+            Assert.AreEqual(2, c.GetValue<int>("dict1","D","T"));
         }
 
         [Test(Description = "Creates a model with a triangle mesh")]

+ 4 - 2
tests/SharpGLTF.Tests/Schema2/LoadAndSave/LoadSampleTests.cs

@@ -32,10 +32,12 @@ namespace SharpGLTF.Schema2.LoadAndSave
 
             ModelRoot model = null;
 
+            var settings = tryFix ? Validation.ValidationMode.TryFix : Validation.ValidationMode.Strict;
+
+            model = ModelRoot.Load(f, settings);
+
             try
             {
-                var settings = tryFix ? Validation.ValidationMode.TryFix : Validation.ValidationMode.Strict;
-
                 model = ModelRoot.Load(f, settings);
                 Assert.NotNull(model);
             }

+ 1 - 1
tests/SharpGLTF.Tests/SharpGLTF.Core.Tests.csproj

@@ -26,7 +26,7 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
   </ItemGroup>  
 
 </Project>

+ 1 - 1
tests/SharpGLTF.Toolkit.Tests/SharpGLTF.Toolkit.Tests.csproj

@@ -26,7 +26,7 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
   </ItemGroup>  
 
 </Project>