// The MIT License (MIT) // // Copyright (c) 2018 Alex Parker // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. namespace Nakama.TinyJson { using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using System.Text; // Really simple JSON parser in ~300 lines // - Attempts to parse JSON files with minimal GC allocation // - Nice and simple "[1,2,3]".FromJson>() API // - Classes and structs can be parsed too! // class Foo { public int Value; } // "{\"Value\":10}".FromJson() // - Can parse JSON without type information into Dictionary and List e.g. // "[1,2,3]".FromJson().GetType() == typeof(List) // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) // - No JIT Emit support to support AOT compilation on iOS // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. // - Only public fields and property setters on classes/structs will be written to // // Limitations: // - No JIT Emit support to parse structures quickly // - Limited to parsing <2GB JSON files (due to int.MaxValue) // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. public static class JsonParser { [ThreadStatic] private static Stack> _splitArrayPool; [ThreadStatic] private static StringBuilder _stringBuilder; [ThreadStatic] private static Dictionary> _fieldInfoCache; [ThreadStatic] private static Dictionary> _propertyInfoCache; public static T FromJson(this string json) { // Initialize, if needed, the ThreadStatic variables if (_propertyInfoCache == null) { _propertyInfoCache = new Dictionary>(); } if (_fieldInfoCache == null) { _fieldInfoCache = new Dictionary>(); } if (_stringBuilder == null) { _stringBuilder = new StringBuilder(); } if (_splitArrayPool == null) { _splitArrayPool = new Stack>(); } // Remove all whitespace not within strings to make parsing simpler _stringBuilder.Length = 0; for (var i = 0; i < json.Length; i++) { var c = json[i]; if (c == '"') { i = AppendUntilStringEnd(true, i, json); continue; } if (char.IsWhiteSpace(c)) continue; _stringBuilder.Append(c); } //Parse the thing! return (T) ParseValue(typeof(T), _stringBuilder.ToString()); } private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) { _stringBuilder.Append(json[startIdx]); for (var i = startIdx + 1; i < json.Length; i++) { if (json[i] == '\\') { if (appendEscapeCharacter) _stringBuilder.Append(json[i]); _stringBuilder.Append(json[i + 1]); i++; //Skip next character as it is escaped } else if (json[i] == '"') { _stringBuilder.Append(json[i]); return i; } else { _stringBuilder.Append(json[i]); } } return json.Length - 1; } //Splits { :, : } and [ , ] into a list of strings private static List Split(string json) { var splitArray = _splitArrayPool.Count > 0 ? _splitArrayPool.Pop() : new List(); splitArray.Clear(); if (json.Length == 2) return splitArray; var parseDepth = 0; _stringBuilder.Length = 0; for (var i = 1; i < json.Length - 1; i++) { if (json[i] == '[' || json[i] == '{') { parseDepth++; } else if (json[i] == ']' || json[i] == '}') { parseDepth--; } else if (json[i] == '"') { i = AppendUntilStringEnd(true, i, json); continue; } else if (json[i] == ',' || json[i] == ':') { if (parseDepth == 0) { splitArray.Add(_stringBuilder.ToString()); _stringBuilder.Length = 0; continue; } } _stringBuilder.Append(json[i]); } splitArray.Add(_stringBuilder.ToString()); return splitArray; } private static object ParseValue(Type type, string json) { if (type == typeof(string)) { if (json.Length <= 2) return string.Empty; var parseStringBuilder = new StringBuilder(json.Length); for (var i = 1; i < json.Length - 1; ++i) { if (json[i] == '\\' && i + 1 < json.Length - 1) { var j = "\"\\nrtbf/".IndexOf(json[i + 1]); if (j >= 0) { parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); ++i; continue; } if (json[i + 1] == 'u' && i + 5 < json.Length - 1) { uint c; if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) { parseStringBuilder.Append((char) c); i += 5; continue; } } } parseStringBuilder.Append(json[i]); } return parseStringBuilder.ToString(); } if (type.IsPrimitive) { var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); return result; } if (type == typeof(decimal)) { decimal result; decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } if (json == "null") { return null; } if (type.IsEnum) { if (json[0] == '"') json = json.Substring(1, json.Length - 2); try { return Enum.Parse(type, json, false); } catch { return 0; } } if (type.IsArray) { var arrayType = type.GetElementType(); if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var newArray = Array.CreateInstance(arrayType, elems.Count); for (var i = 0; i < elems.Count; i++) newArray.SetValue(ParseValue(arrayType, elems[i]), i); _splitArrayPool.Push(elems); return newArray; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { var listType = type.GetGenericArguments()[0]; if (json[0] != '[' || json[json.Length - 1] != ']') return null; var elems = Split(json); var list = (IList) type.GetConstructor(new Type[] {typeof(int)}).Invoke(new object[] {elems.Count}); foreach (var t in elems) list.Add(ParseValue(listType, t)); _splitArrayPool.Push(elems); return list; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { Type keyType, valueType; { var args = type.GetGenericArguments(); keyType = args[0]; valueType = args[1]; } //Refuse to parse dictionary keys that aren't of type string if (keyType != typeof(string)) return null; //Must be a valid dictionary element if (json[0] != '{' || json[json.Length - 1] != '}') return null; //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return null; var dictionary = (IDictionary) type.GetConstructor(new Type[] {typeof(int)}) .Invoke(new object[] {elems.Count / 2}); for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var keyValue = elems[i].Substring(1, elems[i].Length - 2); var val = ParseValue(valueType, elems[i + 1]); dictionary.Add(keyValue, val); } return dictionary; } if (type == typeof(object)) { return ParseAnonymousValue(json); } if (json[0] == '{' && json[json.Length - 1] == '}') { return ParseObject(type, json); } return null; } private static object ParseAnonymousValue(string json) { if (json.Length == 0) return null; if (json[0] == '{' && json[json.Length - 1] == '}') { var elems = Split(json); if (elems.Count % 2 != 0) return null; var dict = new Dictionary(elems.Count / 2); for (var i = 0; i < elems.Count; i += 2) dict.Add(elems[i].Substring(1, elems[i].Length - 2), ParseAnonymousValue(elems[i + 1])); return dict; } if (json[0] == '[' && json[json.Length - 1] == ']') { var items = Split(json); var finalList = new List(items.Count); foreach (var t in items) finalList.Add(ParseAnonymousValue(t)); return finalList; } if (json[0] == '"' && json[json.Length - 1] == '"') { var str = json.Substring(1, json.Length - 2); return str.Replace("\\", string.Empty); } if (char.IsDigit(json[0]) || json[0] == '-') { if (json.Contains(".")) { double result; double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); return result; } else { int result; int.TryParse(json, out result); return result; } } if (json == "true") return true; if (json == "false") return false; // handles json == "null" as well as invalid JSON return null; } private static Dictionary CreateMemberNameDictionary(IEnumerable members) where T : MemberInfo { // NOTE The StringComparer is disabled intentionally because of how our generated code. // var nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); var nameToMember = new Dictionary(); foreach (var member in members) { if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) continue; var name = member.Name; if (member.IsDefined(typeof(DataMemberAttribute), true)) { var dataMemberAttribute = (DataMemberAttribute) Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) name = dataMemberAttribute.Name; } nameToMember.Add(name, member); } return nameToMember; } private static object ParseObject(Type type, string json) { var instance = FormatterServices.GetUninitializedObject(type); // The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON var elems = Split(json); if (elems.Count % 2 != 0) return instance; Dictionary nameToField; Dictionary nameToProperty; if (!_fieldInfoCache.TryGetValue(type, out nameToField)) { nameToField = CreateMemberNameDictionary( type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _fieldInfoCache.Add(type, nameToField); } if (!_propertyInfoCache.TryGetValue(type, out nameToProperty)) { nameToProperty = CreateMemberNameDictionary( type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); _propertyInfoCache.Add(type, nameToProperty); } for (var i = 0; i < elems.Count; i += 2) { if (elems[i].Length <= 2) continue; var key = elems[i].Substring(1, elems[i].Length - 2); var value = elems[i + 1]; FieldInfo fieldInfo; PropertyInfo propertyInfo; if (nameToField.TryGetValue(key, out fieldInfo)) fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); else if (nameToProperty.TryGetValue(key, out propertyInfo)) propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); } return instance; } } }