Saving and loading Dictionary<> from a file

Hello! My game map is a Dictionary<>, which I’ve just found out Unity’s inbuild JSON totally ignores. I’m not sure why this is, but is there a way around this? I just need a reliable way to save data. Preferably one that discourages player tampering & avoids weird workarounds like turning it into a bunch of lists.

Hi I currently use MiniJson to encode / decode Dictionary<string,object> to JSON and it work well.

Here is my version, maybe there are newer version somewhere on internet :wink:

/*
 * Copyright (c) 2013 Calvin Rien
 *
 * Based on the JSON parser by Patrick van Bergen
 * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
 *
 * Simplified it so that it doesn't throw exceptions
 * and can be used in Unity iPhone with maximum code stripping.
 *
 * 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.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Utilities
{
    // Example usage:
    //
    //  using UnityEngine;
    //  using System.Collections;
    //  using System.Collections.Generic;
    //  using MiniJSON;
    //
    //  public class MiniJSONTest : MonoBehaviour {
    //      void Start () {
    //          var jsonString = "{ \"array\": [1.44,2,3], " +
    //                          "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
    //                          "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
    //                          "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
    //                          "\"int\": 65536, " +
    //                          "\"float\": 3.1415926, " +
    //                          "\"bool\": true, " +
    //                          "\"null\": null }";
    //
    //          var dict = Json.Deserialize(jsonString) as Dictionary<string,object>;
    //
    //          Debug.Log("deserialized: " + dict.GetType());
    //          Debug.Log("dict['array'][0]: " + ((List<object>) dict["array"])[0]);
    //          Debug.Log("dict['string']: " + (string) dict["string"]);
    //          Debug.Log("dict['float']: " + (double) dict["float"]); // floats come out as doubles
    //          Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
    //          Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
    //
    //          var str = Json.Serialize(dict);
    //
    //          Debug.Log("serialized: " + str);
    //      }
    //  }

    /// <summary>
    /// This class encodes and decodes JSON strings.
    /// Spec. details, see http://www.json.org/
    ///
    /// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
    /// All numbers are parsed to doubles.
    /// </summary>
    public static class MiniJSON
    {
        /// <summary>
        /// Parses the string json into a value
        /// </summary>
        /// <param name="json">A JSON string.</param>
        /// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a double, an integer,a string, null, true, or false</returns>
        public static object Deserialize( string json )
        {
            try
            {
                // save the string for debug information
                return json == null ? null : Parser.Parse( json );
            }
            catch( Exception )
            {
                return null;
            }
        }

        private sealed class Parser : IDisposable
        {
            private const string WORD_BREAK = "{}[],:\"";

            private static bool IsWordBreak( char c )
            {
                return char.IsWhiteSpace( c ) || WORD_BREAK.IndexOf( c ) != -1;
            }

            private const string HEX_DIGIT = "0123456789ABCDEFabcdef";

            private static bool IsHexDigit( char c )
            {
                return HEX_DIGIT.IndexOf( c ) != -1;
            }

            private enum TOKEN
            {
                NONE,
                CURLY_OPEN,
                CURLY_CLOSE,
                SQUARED_OPEN,
                SQUARED_CLOSE,
                COLON,
                COMMA,
                STRING,
                NUMBER,
                TRUE,
                FALSE,
                NULL
            };

            private StringReader json;

            private Parser( string jsonString )
            {
                json = new StringReader( jsonString );
            }

            public static object Parse( string jsonString )
            {
                using( var instance = new Parser( jsonString ) )
                {
                    return instance.ParseValue();
                }
            }

            public void Dispose()
            {
                json.Dispose();
                json = null;
            }

            private Dictionary<string, object> ParseObject()
            {
                var table = new Dictionary<string, object>();

                // ditch opening brace
                json.Read();

                // {
                while( true )
                {
                    switch( NextToken )
                    {
                        case TOKEN.NONE:
                            return null;
                        case TOKEN.COMMA:
                            continue;
                        case TOKEN.CURLY_CLOSE:
                            return table;
                        case TOKEN.STRING:
                            // name
                            var name = ParseString();
                            if( name == null )
                            {
                                return null;
                            }

                            // :
                            if( NextToken != TOKEN.COLON )
                            {
                                return null;
                            }
                            // ditch the colon
                            json.Read();

                            // value
                            var valueToken = NextToken;
                            var value = ParseByToken( valueToken );
                            if( value == null && valueToken != TOKEN.NULL )
                                return null;
                            table[name] = value;
                            break;
                        default:
                            return null;
                    }
                }
            }

            private List<object> ParseArray()
            {
                var array = new List<object>();

                // ditch opening bracket
                json.Read();

                // [
                var parsing = true;
                while( parsing )
                {
                    var nextToken = NextToken;

                    switch( nextToken )
                    {
                        case TOKEN.NONE:
                            return null;
                        case TOKEN.COMMA:
                            continue;
                        case TOKEN.SQUARED_CLOSE:
                            parsing = false;
                            break;
                        default:
                            var value = ParseByToken( nextToken );
                            if( value == null && nextToken != TOKEN.NULL )
                                return null;
                            array.Add( value );
                            break;
                    }
                }

                return array;
            }

            private object ParseValue()
            {
                var nextToken = NextToken;
                return ParseByToken( nextToken );
            }

            private object ParseByToken( TOKEN token )
            {
                switch( token )
                {
                    case TOKEN.STRING:
                        return ParseString();
                    case TOKEN.NUMBER:
                        return ParseNumber();
                    case TOKEN.CURLY_OPEN:
                        return ParseObject();
                    case TOKEN.SQUARED_OPEN:
                        return ParseArray();
                    case TOKEN.TRUE:
                        return true;
                    case TOKEN.FALSE:
                        return false;
                    case TOKEN.NULL:
                        return null;
                    default:
                        return null;
                }
            }

            private string ParseString()
            {
                var s = new StringBuilder();
                char c;

                // ditch opening quote
                json.Read();

                var parsing = true;
                while( parsing )
                {
                    if( json.Peek() == -1 )
                    {
                        break;
                    }

                    c = NextChar;
                    switch( c )
                    {
                        case '"':
                            parsing = false;
                            break;
                        case '\\':
                            if( json.Peek() == -1 )
                            {
                                parsing = false;
                                break;
                            }

                            c = NextChar;
                            switch( c )
                            {
                                case '"':
                                case '\\':
                                case '/':
                                    s.Append( c );
                                    break;
                                case 'b':
                                    s.Append( '\b' );
                                    break;
                                case 'f':
                                    s.Append( '\f' );
                                    break;
                                case 'n':
                                    s.Append( '\n' );
                                    break;
                                case 'r':
                                    s.Append( '\r' );
                                    break;
                                case 't':
                                    s.Append( '\t' );
                                    break;
                                case 'u':
                                    var hex = new char[4];

                                    for( var i = 0; i < 4; i++ )
                                    {
                                        hex[i] = NextChar;
                                        if( !IsHexDigit( hex[i] ) )
                                            return null;
                                    }

                                    s.Append( (char)Convert.ToInt32( new string( hex ), 16 ) );
                                    break;
                            }
                            break;
                        default:
                            s.Append( c );
                            break;
                    }
                }

                return s.ToString();
            }

            private object ParseNumber()
            {
                var number = NextWord;

                if( number.IndexOf( '.' ) == -1 && number.IndexOf( 'E' ) == -1 && number.IndexOf( 'e' ) == -1 )
                {
                    long.TryParse( number, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var parsedInt );
                    return parsedInt;
                }

                double.TryParse( number, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var parsedDouble );
                return parsedDouble;
            }

            private void EatWhitespace()
            {
                while( char.IsWhiteSpace( PeekChar ) )
                {
                    json.Read();

                    if( json.Peek() == -1 )
                    {
                        break;
                    }
                }
            }

            private char PeekChar => Convert.ToChar( json.Peek() );

            private char NextChar => Convert.ToChar( json.Read() );

            private string NextWord
            {
                get
                {
                    var word = new StringBuilder();

                    while( !IsWordBreak( PeekChar ) )
                    {
                        word.Append( NextChar );

                        if( json.Peek() == -1 )
                        {
                            break;
                        }
                    }

                    return word.ToString();
                }
            }

            private TOKEN NextToken
            {
                get
                {
                    EatWhitespace();

                    if( json.Peek() == -1 )
                    {
                        return TOKEN.NONE;
                    }

                    switch( PeekChar )
                    {
                        case '{':
                            return TOKEN.CURLY_OPEN;
                        case '}':
                            json.Read();
                            return TOKEN.CURLY_CLOSE;
                        case '[':
                            return TOKEN.SQUARED_OPEN;
                        case ']':
                            json.Read();
                            return TOKEN.SQUARED_CLOSE;
                        case ',':
                            json.Read();
                            return TOKEN.COMMA;
                        case '"':
                            return TOKEN.STRING;
                        case ':':
                            return TOKEN.COLON;
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                        case '-':
                            return TOKEN.NUMBER;
                    }

                    switch( NextWord )
                    {
                        case "false":
                            return TOKEN.FALSE;
                        case "true":
                            return TOKEN.TRUE;
                        case "null":
                            return TOKEN.NULL;
                    }

                    return TOKEN.NONE;
                }
            }
        }

        /// <summary>
        /// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
        /// </summary>
        /// <param name="obj">A Dictionary&lt;string, object&gt; / List&lt;object&gt;</param>
        /// <param name="encodeChars"></param>
        /// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
        public static string Serialize( object obj, bool encodeChars = false )
        {
            return Serializer.Serialize( obj, encodeChars );
        }

        private sealed class Serializer
        {
            private readonly StringBuilder builder;
            private bool encodeChars = false;
            
            private Serializer()
            {
                builder = new StringBuilder();
            }

            public static string Serialize( object obj, bool encodeChars = false )
            {
                var instance = new Serializer {encodeChars = encodeChars};

                instance.SerializeValue( obj );

                return instance.builder.ToString();
            }

            private void SerializeValue( object value )
            {
                Array asArray;
                IList asList;
                IDictionary asDict;
                string asStr;

                if( value == null )
                {
                    builder.Append( "null" );
                }
                else if( ( asStr = value as string ) != null )
                {
                    SerializeString( asStr );
                }
                else if( value is bool )
                {
                    builder.Append( (bool)value ? "true" : "false" );
                }
                else if( (asArray = value as Array) != null && asArray.Rank > 1 )
                {
                    throw new Exception("array "+asArray.Rank+" dimensions non implémentés, utiliser le ToJaggedArray dans Extensions methods");
                }
                else if( ( asList = value as IList ) != null )
                {
                    SerializeArray( asList );
                }
                else if( ( asDict = value as IDictionary ) != null )
                {
                    SerializeObject( asDict );
                }
                else if( value is char )
                {
                    SerializeString( new string( (char)value, 1 ) );
                }
                else
                {
                    SerializeOther( value );
                }
            }

            private void SerializeObject( IDictionary obj )
            {
                var first = true;

                builder.Append( '{' );

                foreach( var e in obj.Keys )
                {
                    if( !first )
                    {
                        builder.Append( ',' );
                    }

                    SerializeString( e.ToString() );
                    builder.Append( ':' );

                    SerializeValue( obj[e] );

                    first = false;
                }

                builder.Append( '}' );
            }

            private void SerializeArray( IList anArray )
            {
                builder.Append( '[' );

                var first = true;

                for( var i = 0; i < anArray.Count; i++ )
                {
                    var obj = anArray[i];
                    if( !first )
                    {
                        builder.Append( ',' );
                    }

                    SerializeValue( obj );

                    first = false;
                }

                builder.Append( ']' );
            }
            
            private void SerializeString( string str )
            {
                builder.Append( '\"' );

                var charArray = str.ToCharArray();
                for( var i = 0; i < charArray.Length; i++ )
                {
                    var c = charArray[i];
                    switch( c )
                    {
                        case '"':
                            builder.Append( "\\\"" );
                            break;
                        case '\\':
                            builder.Append( "\\\\" );
                            break;
                        case '\b':
                            builder.Append( "\\b" );
                            break;
                        case '\f':
                            builder.Append( "\\f" );
                            break;
                        case '\n':
                            builder.Append( "\\n" );
                            break;
                        case '\r':
                            builder.Append( "\\r" );
                            break;
                        case '\t':
                            builder.Append( "\\t" );
                            break;
                        default:
                            var codepoint = Convert.ToInt32( c );
                            if( !encodeChars || ( codepoint >= 32  &&  codepoint <= 126 ) )
                            {
                                builder.Append( c );
                            }
                            else
                            {
                                builder.Append( "\\u" );
                                builder.Append( codepoint.ToString( "x4" ) );
                            }
                            break;
                    }
                }

                builder.Append( '\"' );
            }

            private void SerializeOther( object value )
            {
                // NOTE: decimals lose precision during serialization.
                // They always have, I'm just letting you know.
                // Previously floats and doubles lost precision too.
                if( value is float )
                {
                    builder.Append( ( (float)value ).ToString( "R", System.Globalization.CultureInfo.InvariantCulture ) );
                }
                else if( value is int
                  || value is uint
                  || value is long
                  || value is sbyte
                  || value is byte
                  || value is short
                  || value is ushort
                  || value is ulong )
                {
                    builder.Append( value );
                }
                else if( value is double
                  || value is decimal )
                {
                    builder.Append( Convert.ToDouble( value ).ToString( "R", System.Globalization.CultureInfo.InvariantCulture ) );
                }
                else
                {
                    SerializeString( value.ToString() );
                }
            }
        }

        private const string INDENT_STRING = "    ";

        public static string FormatJson( string str )
        {
            var indent = 0;
            var quoted = false;
            var sb = new StringBuilder();
            for( var i = 0; i < str.Length; i++ )
            {
                var ch = str[i];
                switch( ch )
                {
                    case '{':
                    case '[':
                        sb.Append( ch );
                        if( !quoted )
                        {
                            sb.AppendLine();
                            Enumerable.Range( 0, ++indent ).ForEach( item => sb.Append( INDENT_STRING ) );
                        }
                        break;
                    case '}':
                    case ']':
                        if( !quoted )
                        {
                            sb.AppendLine();
                            Enumerable.Range( 0, --indent ).ForEach( item => sb.Append( INDENT_STRING ) );
                        }
                        sb.Append( ch );
                        break;
                    case '"':
                        sb.Append( ch );
                        var escaped = false;
                        var index = i;
                        while( index > 0 && str[--index] == '\\' )
                            escaped = !escaped;
                        if( !escaped )
                            quoted = !quoted;
                        break;
                    case ',':
                        sb.Append( ch );
                        if( !quoted )
                        {
                            sb.AppendLine();
                            Enumerable.Range( 0, indent ).ForEach( item => sb.Append( INDENT_STRING ) );
                        }
                        break;
                    case ':':
                        sb.Append( ch );
                        if( !quoted )
                            sb.Append( " " );
                        break;
                    default:
                        sb.Append( ch );
                        break;
                }
            }
            return sb.ToString();
        }
    }
}

But it’s JSON data, your users will easily modify it :wink:
You can add some sort of encryption on the JSON string or make your own serialization format.

Thanks a lot for the plugin, but there seem to be a couple of errors in it:

Oh sorry I modified it a little bit, and I use custom ExtensionMethods.

you can add this method inside the class, it should work:


        /// <summary>
        /// foreach helper
        /// </summary>
        /// <param name="ie"></param>
        /// <param name="action"></param>
        /// <typeparam name="T"></typeparam>
        public static void ForEach<T>( this IEnumerable<T> ie, Action<T> action )
        {
            foreach( var i in ie )
            {
                action( i );
            }
        }

I never tried it but unity have built in serialization system, you could try it, it supports binary serialization that would be much harder for your players to modify :wink:

Thank you, but I’m kinda struggling to work out how to use the things you’ve sent with a Dictionary. If possible I would like to use Unity’s inbuilt serialization, but I can’t work out what package they’re using in that link or how that would apply to a dictionary. Are there any complete examples?

Unity’s inbuilt serialization does not support dictionaries. If you still want to stay with inbuilt, you either have to serialize 2 separate lists, one for the keys and one for the values, or else, you can make your own serializable type that has a field for the key and another field for the value, and then serialize a list of that type.

Are you sure? It took a while to find the package, but I’m reading the save file and it seems to have done it. It’s saving the key and the value for the test entries I’ve added.

I think you may mean [Serialize]. This seems to be something else.

If it’s done it, it’s not using Unity’s serialization for dictionaries then, that’s what I meant.

Yes, the serialization package can deal with dictionaries out of the box. Just be sure to check the rules for a field to be serialized (it relies on the Unity.Properties module), check the chapters “Describing Types for Serialization” and “Additional Attributes” on the package manual

Thank you. It seems to be working well right now, but I will check those first. Just wondering why this isn’t standard as I’m sure lots of people need to save and load dictionaries.

We’ve all been wondering that same obvious question for ten-plus years now.

I have seen dozens posts by people who wasted days with this thinking that something they were doing is wrong.

It’s such a grievously inexcusable waste of the Unity users community time that I have a blurb ready for it:

Problems with Unity “tiny lite” built-in JSON:

In general I highly suggest staying away from Unity’s JSON “tiny lite” package. It’s really not very capable at all and will silently fail on very common data structures, such as bare arrays, tuples, Dictionaries and Hashes and ALL properties.

Instead grab Newtonsoft JSON .NET from the Unity Package Manager (Window → Package Manager). You presently have to add it by name from the UPM and use com.unity.nuget.newtonsoft-json

If you want to avoid the Package Mangler’s UI, just add this to your Packages/manifest.json file:

"com.unity.nuget.newtonsoft-json": "3.0.2",

The former asset store package is now deprecated, as you can see here:

Also, when working with JSON, always be sure to leverage sites like:

https://csharptojson.com

That is incredibly baffling and stupid and I don’t get it either. But thank you.

It’s the kind of stupid issue where engineers who have more experience (eg, “hey, I’ve used JSON before, let’s use it in Unity!”) are actually at a disadvantage because they come in assuming Unity JSON will be “just like every other JSON,” because, come on, why wouldn’t it be?!

They spend so much time thinking “I must be doing this wrong…” At a minimum there should be SOME kind of warning, perhaps editor only, but I guess that can’t be because it’s just a thin wrapper around the same serialization system the editor itself uses.

Meh, such a waste of every developer’s time.

I’m not even hugely experienced, and I know it has wasted multiple days of my time!

Is there any way we could all protest it and get them to maybe try to change it? Makes no sense we can’t at least save a dang Dictionary<>. Or the Time.

I mean, you have infinite workarounds, just googling “Unity serialize dictionary” will give you many solutions.

I agree is not ideal, probably they doesn’t even have a proper reason for not implementing it, but i doubt at this stage it will change unless its together with another big change that requires them to do it

Yeah, but why would Unity force every game designer to use a workaround when the simple solution is to just support this?

I guess I could launch a bug report. I still have the unworking version backed up.

Also I now have a bug where I can save & load a file, but can’t modify and then save over the old file then reload it.