Unity XML Deserializer

Would this be useful to anyone?

I wrote a lightweight de-serializer, because I wanted to be able to load data from simple XML files into some GameObjects I had. It’ll handle Unity Behaviours as if they’re data structures (so it ignores all Unity-specific gunk, but allows you to read in stuff into your Components as if its an object). The only thing I haven’t tested is whether it handles dictionaries.

It’s not hugely feature complete, but I suspect it’ll be a better place to start from than scratch for most people solving the same problem.

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Xml;

namespace UnityExtensions.Xml.Serialization
{
    //A simple config XML serializer/deserializer
    //Intended mainly for reading to and writing from MonoBehaviours
    /*
     * This class is very complicated and it needs some explaining
     *
     * Generic programming in C# means that passing a template to this serializer puts constraints on it.
     * Unity makes those contraints impossible to enforce across all the objects you'd need to work on.
     *
     * Unity requires that no behaviour have a new() function.  All behaviours have to be added to a
     * GameObject, so that makes a lot of sense.
     *
     * However, if you're deserializing a straightforwards object, then you need a new() constructor.
     * This is self-evident: how else will you create a default object to put parameters into?
     *
     * This means that, if you want to make a serializer that can handle both objects and Unity behaviours
     * you have to either detect them separately and handle them separately, or basically use C# reflection
     * to bypass EVERYTHING.
     *
     * I went with the second option because, although the Reflection code is messy as hell, the recursive
     * call flow of the code is easier to visualise.  It's easy to create a new serializer for any type because
     * the serializer IS the serializer for that template.  No detection, simple code flow.  All the evil is
     * inside the deserialization function, where it should be.
     *
     * The biggest problem with the code is that no assumptions can be made about the template type in the code.
     * C# will throw an error if any type of assumption is made in the code.  So T Obj = new T() assumes newable,
     * casting to T assumes it is a class and so on.  This is why every call is made using reflection.
     *
     * Also, sticking to C# 3.5 means its impossible to create and cast to generic objects.  This is a known
     * issue and the way around this is the dynamic keyword introduced in C# 4.0.  However, we can't use that, so
     * maintaining all serializer instances as objects and using reflection on them works very well.
     *
     */
    public class UnityXmlSerializer<T> {
        public Type    type = typeof(T);
        public Dictionary<string, FieldInfo>    fields;
        public Dictionary<string, PropertyInfo>    properties;

        private static Type    SerTypeTemplate = typeof( UnityXmlSerializer<> );
        bool                UnityType = false;

        //Damn, I thought I'd made this static?  Never mind...
        private Dictionary<Type, object >    serializerCache;

        //These reflect the main types a serialiser will have to deal with and implement
        public enum uXmlSerType
        {
            unknownType, //Ain't got there yet;
            basicType,    //TypeCode not an object
            enumType,    //TypeCode an object and (something else I haven't figure out)
            arrayType,    //Describes IList
            dictionaryType,    //Describes dictionary - TODO
            objectType,    //Is an object and is not any of the other types
            unityType,    //Is a Unity Component type object which must be on a Game Object
        }

        public uXmlSerType    unitySerType;

        //I still don't trsut this not to crap out on me, so for now
        public int debug = 0;

        //Used publically
        public UnityXmlSerializer()
        {
            Init ();

            //Create serializer cache
            serializerCache = new Dictionary<Type, object>();

            //Add self to cache
            serializerCache [type] = this;
        }

        //Used by the serializer when creating different versions of itself in the child code
        public UnityXmlSerializer( Dictionary<Type, object> cache )
        {
            Init ();

            serializerCache = cache;
        }

        public UnityXmlSerializer( Dictionary<Type, object> cache, int debugLevel )
        {
            debug = debugLevel;

            Init ();

            serializerCache = cache;
        }

        /// <summary>
        /// Init this instance.
        /// Contains parts of the constructors which are common across all of them
        /// </summary>
        private void Init() {
            if( debug>=1)
                Debug.Log ("XMLSerialization Test: Constructor starting on type " + type.Name);
            if (Type.GetTypeCode(type) != TypeCode.Object) {
                if( type.IsEnum )
                    unitySerType = uXmlSerType.enumType;
                else
                    unitySerType = uXmlSerType.basicType;
            } else {
                if(type.GetInterface( "IList") != null )
                {
                    unitySerType = uXmlSerType.arrayType;
                }
                if(type.GetInterface ( "IDictionary" ) != null )
                    unitySerType = uXmlSerType.dictionaryType;
              
                if( type.IsEnum )
                    unitySerType = uXmlSerType.enumType;
              
                //Check fields for descent from Unity
                fields = new Dictionary<string, FieldInfo> ();
                properties = new Dictionary<string, PropertyInfo> ();
                foreach (FieldInfo FI in type.GetFields()) {
                    if (IsUnityBaseClass (FI.DeclaringType.Name)) {
                        UnityType = true;
                    } else {
                        if(debug>=2)
                            Debug.Log ("XMLSerialization Test: Constructor: Added field " + FI.Name + " to the dictionary which was declared in " + FI.DeclaringType.Name);
                        fields.Add (FI.Name, FI);
                    }
                }
              
                foreach (PropertyInfo PI in type.GetProperties ( )) {
                    //Automatically filter out MonoBehaviour and Object stuff - We only want anything after that.
                    if (IsUnityBaseClass (PI.DeclaringType.Name)) {
                        UnityType = true;
                    } else {
                        if(debug>=2)
                            Debug.Log ("XMLSerialization Test: Constructor: Added property " + PI.Name + " to the dictionary which was declared in " + PI.DeclaringType.Name);
                        properties.Add (PI.Name, PI);
                    }
                }
              
                if( UnityType )
                    unitySerType =   uXmlSerType.unityType;
              
                //If all other options have fallen through
                if( unitySerType == uXmlSerType.unknownType )
                {
                    unitySerType = uXmlSerType.objectType;
                }
              
              
            }

            //Prints full list of everything there for really hard testing purposes
            if (debug >= 4) {
                foreach (FieldInfo FI in type.GetFields ()) {
                    Debug.Log ("XMLSerialization Test: Constructor field list: " + FI.Name);
                }

                foreach (PropertyInfo PI in type.GetProperties ()) {
                    Debug.Log ("XMLSerialization Test: Constructor property list: " + PI.Name);
                }

                foreach (MemberInfo MI in type.GetMembers()) {
                    Debug.Log ("XMLSerialization Test: Constructor member list: " + MI.Name);
                }
            }
            if (debug >= 3) {
                Debug.Log ("XmlSerializer: Constructor:  Finishing with type " + type.Name + " which we think is " + unitySerType);
                Debug.Log ("XmlSerializer: Constructor: We know that: UnityType - " + UnityType + "; TypeCode - " + Type.GetTypeCode (type) + " and  enum - " + type.IsEnum);
            }

        }

        /*public void    Serialize( T Object, string name, XmlNode parent )
        {
            if( debug>=1)
                Debug.Log ("XMLSerialization: Serialize");

            if (unitySerType == uXmlSerType.unityType || unitySerType == uXmlSerType.objectType) {
                if(debug>=2)
                    Debug.Log ("XMLSerialization: Starting unity or object type deserialization");
                XmlNode    element;

                foreach (KeyValuePair<string,FieldInfo> field in fields) {
                    object serializer = GetSerializer (field.Value.FieldType);
                    element = parent.OwnerDocument.CreateNode ("element", field.Key, "" );
                    parent.AppendChild (element);

                    object[] objs = new object[] { field.Value.GetValue(), field.Key, element };
                    serializer.GetType ().GetMethod ("Serialize").Invoke (serializer, objs);
                }

                foreach (KeyValuePair<string, PropertyInfo> property in properties) {
                    object serializer = GetSerializer (property.Value.PropertyType);
                    element = parent.OwnerDocument.CreateNode ("element", property.Key, "" );
                    parent.AppendChild (element);

                    object[] objs = new object[] { property.Value.GetValue(), property.Key, element };
                    serializer.GetType ().GetMethod ("Serialize").Invoke (serializer, objs);
                }
              
            } else if (unitySerType == uXmlSerType.basicType) {
                if(debug>=2)
                    Debug.Log ("XMLSerialization: Starting basic type serialization");

                MethodInfo convertMethod;

                if( Type.GetTypeCode(type) != TypeCode.String )
                    convertMethod = typeof(XmlConvert).GetMethod ( "ToString", new Type[] {typeof(T)} );
                else
                    convertMethod = typeof(string).GetMethod("Clone");

                if( convertMethod == null )
                {
                    //Error
                }
                else
                {
                    string str;
                    if( Type.GetTypeCode (type)!= TypeCode.String )
                        str = (string)convertMethod.Invoke( null, new object[] { Object } );
                    else
                        str = (string)convertMethod.Invoke ( null, new object[]{ Object } );
                }
            }
        }*/

        public T    Deserialize( GameObject GO, XmlNode E )
        {
            if( debug>=1)
                Debug.Log ("XMLSerialization: Deserialize");

            //All of this needs way more error checking considering just how much stuff I bypass
            T newVar;

            if (unitySerType == uXmlSerType.unityType) {
                Type goType = GO.GetType ();
                MethodInfo castMethod = goType.GetMethod ("AddComponent", new Type[0]).MakeGenericMethod (type);
                newVar = (T)castMethod.Invoke (GO, null);
                //newVar = GO.AddComponent (typeof(T)) as T;
                if( debug>=2 )
                    Debug.Log ("XMLSerialization: Created unity component");
            } else if (unitySerType == uXmlSerType.basicType) {
                if(debug>=1)
                    Debug.Log ("XMLSerialization: Initiallised basic type");
                newVar = default(T);
                if( Type.GetTypeCode (type) == TypeCode.String  )
                {
                    if( newVar == null && debug>=4 )
                    {
                        Debug.Log ("So an empty string evaluates to null then?");    //This bit me bad
                    }
                }
                if(debug>=3)
                    Debug.Log ("XMLSerialization Test: Initiallised default object");
                //Do nothing
            } else if ( unitySerType == uXmlSerType.enumType ){
                newVar = default(T);
            }
            else
            {
                //Handle subclasses held through an interface
                if (E.Attributes != null && E.Attributes ["typeHint"] != null) {
                    Debug.Log ("Parent class interface, forwarding it to the child serializer");
                    object serializer = GetSerializer (Type.GetType(E.Attributes["typeHint"].Value));

                    object[] objs = new object[2] { GO, E };
                    return (T)serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs);
                }

                //Because we're calling stuff manually, check if the blank constructor exists
                ConstructorInfo ctor = type.GetConstructor (new Type[0]);
                if (ctor != null) {
                    newVar = (T)Activator.CreateInstance (typeof(T));    //Gets around Unity's new() blockade
                    if(debug>=2)
                        Debug.Log ("XMLSerialization: Created class instance");    //Super useful to know under some debug circumstances
                } else {
                    Debug.LogError ("XMLSerialization: Deserialize: Unable to find empty contructor for the class you're trying to deserialize.  This is bad.");
                    newVar = default(T);
                }
            }

            //If we get here and have nothing, then push error
            //(note: empty strings evaluate to null and always trip this)
            if (newVar == null && Type.GetTypeCode (type ) != TypeCode.String ) {
                Debug.LogError ("Xml Deserializer: Could not create a new component of type " + type.Name + " in GameObject " + GO.name);
            }

            if (unitySerType == uXmlSerType.basicType) {
                if(debug>=2)
                    Debug.Log ("XMLSerialization: Starting basic type deserialization");

                //We take advantage of the fact that XMLConvert's functions has a naming scheme that fits well with reflective patterns
                //Exception to this is ToString, which just doesn't work
                MethodInfo convertMethod;

                if( Type.GetTypeCode(type) != TypeCode.String )
                    convertMethod = typeof(XmlConvert).GetMethod ( "To" + type.Name );
                else
                    convertMethod = E.InnerText.GetType().GetMethod("Clone");

                if( convertMethod == null )
                {
                    Debug.LogError ( "XmlSerialization: Could not find conversion to specified type " + type.Name + " (we looked for a function To"+type.Name +"() in XmlConvert)" );
                }
                else
                {
                    if( debug>=3 )
                        Debug.Log ("Basic conversion about to start" );
                    if( Type.GetTypeCode (type)!= TypeCode.String )
                        newVar = (T)convertMethod.Invoke( null, new object[] { E.InnerText } );
                    else
                        newVar = (T)convertMethod.Invoke ( E.InnerText, new object[]{} );
                    if(debug>=4)
                        Debug.Log ("Basic type conversion went fine.");
                }
            } else if(unitySerType == uXmlSerType.enumType) {
                //MethodInfo convertMethod = typeof(XmlConvert).GetMethod ( "ToInt32" );
                //newVar = (T)convertMethod.Invoke (null, new object[]{E.InnerText});
                newVar = (T)Enum.Parse ( type, E.InnerText, true );


            } else if (unitySerType == uXmlSerType.unityType || unitySerType == uXmlSerType.objectType) {
                foreach (XmlNode child in E.ChildNodes) {
                    if (fields.ContainsKey (child.Name)) {
                        FieldInfo thisField = fields [child.Name];

                        object serializer = GetSerializer( thisField.FieldType );

                        object[] objs = new object[2] { GO, child };
                        if( debug>=2 )
                            Debug.Log ("Field " + thisField.Name + " exists in this object " + type.Name + " and we're about to deserialize into it" );
                        thisField.SetValue (newVar, serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs));
                    }

                    if (properties.ContainsKey (child.Name)) {
                        PropertyInfo thisProp = properties [child.Name];
                        object serializer = GetSerializer ( thisProp.PropertyType );
                      
                        object[] objs = new object[2] { GO, child };
                        thisProp.SetValue (newVar, serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs), null);
                    }
                }
            } else if (unitySerType == uXmlSerType.arrayType)
            {
                if( type.IsArray )
                {
                    Type elmntType = type.GetElementType();
                    object    serializer = GetSerializer(elmntType);

                    int i=0;
                    foreach( XmlNode child in E.ChildNodes )
                    {
                        i++;
                    }
                    int size = i;

                    object[]    tempArray = new object[i];
                    i=0;
                    foreach( XmlNode child in E.ChildNodes )
                    {
                        object[] objs = new object[2] { GO, child };
                        object temp = serializer.GetType().GetMethod( "Deserialize").Invoke (serializerCache, objs );
                        tempArray[i++] = temp;
                    }

                    MethodInfo resizeMethod = type.GetMethod ("Resize");
                    resizeMethod.Invoke ( newVar, new object[] { size } );

                    IList list = (IList)newVar;

                    i=0;
                    foreach( object o in tempArray )
                    {
                        list[i++] = o;
                    }
                }
                else {
                    IList list = (IList)newVar;

                    Type[]    typeParams = type.GetGenericArguments();
                    object serializer=null;

                    if( list != null || typeParams != null || typeParams.Length == 1 )
                    {
                        serializer = GetSerializer( typeParams[0]);
                    }
                    else
                        Debug.LogError ("XMLSerialization: List section: Problem with List generic arguments, there should be one and only one template argument");

                    foreach( XmlNode child in E.ChildNodes )
                    {
                        object[] objs = new object[2] { GO, child };
                        object temp = serializer.GetType().GetMethod( "Deserialize").Invoke (serializer, objs );
                        list.Add ( temp );
                    }
                }
            } else if(unitySerType == uXmlSerType.dictionaryType )
            {
                IDictionary dict = (IDictionary)newVar;

                Type[]    typeParams = type.GetGenericArguments();
                object     keySerializer = null;
                object    valueSerializer = null;

                if( dict != null || typeParams != null || typeParams.Length == 2 )
                {
                    keySerializer = GetSerializer( typeParams[0]);
                    valueSerializer = GetSerializer(typeParams[1]);
                }

                object[]    pair =new object[2];
                pair[0] = null;
                pair[1] = null;
                foreach( XmlNode child in E.ChildNodes )
                {
                    if( child.Name == "key" )
                    {
                        object[] objs = new object[2] { GO, child };
                        pair[0] = keySerializer.GetType().GetMethod( "Deserialize").Invoke (keySerializer, objs );
                    }
                    if( child.Name == "value" )
                    {
                        object[] objs = new object[2] { GO, child };
                        pair[1] = valueSerializer.GetType().GetMethod( "Deserialize").Invoke (valueSerializer, objs );
                    }
                    if( pair[0] != null && pair[1] != null )
                    {
                        dict.Add ( pair[0], pair[1] );
                        //Prevent spurious adding in the event XML was generated elsewhere and includes other fields
                        pair[0]=null;
                        pair[1] = null;
                    }
                }
            }

            if(debug>=1)
                Debug.Log ("About to return.");
            return newVar;
        }

        //----Useful helper functions----
        //This code fragment is used numerous times
        private    object GetSerializer( Type _type ) {
            object    serializer;
          
            if (serializerCache.ContainsKey (_type)) {
                serializer = serializerCache [_type];
            } else {
                Type newSerType = SerTypeTemplate.MakeGenericType (_type);
                serializer = Activator.CreateInstance (newSerType, serializerCache );
                serializerCache [_type] = serializer;
            }

            return serializer;
        }

        private bool IsUnityBaseClass( string className )
        {
            return ( className == "Object" || className == "Component" || className == "Behaviour" || className == "MonoBehaviour");
        }
    }
}

Also requires (as a dependency):

using System;
using System.Collections;
using System.Collections.Generic;

using UnityEngine;

//Call explicitly
namespace UnityExtensions
{
    public class Hierarchy {

        //Children
        public static System.Collections.Generic.IEnumerable<GameObject>
            Children( GameObject Parent )
        {
            //GameObject GO;
            for (int i=0; i<Parent.transform.childCount; ++i)
                yield return Parent.transform.GetChild (i).gameObject;
        }

        public static GameObject    Parent( GameObject GO )
        {
            if(GO.transform.parent != null )
                return GO.transform.parent.gameObject;
            else
                return null;
        }

        public static GameObject    FirstChild( GameObject GO )
        {
            return GO.transform.GetChild (0).gameObject;
        }

        public static GameObject    SecondChild( GameObject GO )
        {
            return GO.transform.GetChild (1).gameObject;
        }

        public static GameObject    LastChild( GameObject GO )
        {
            return GO.transform.GetChild ( GO.transform.childCount-1 ).gameObject;
        }

        //Find
        public static GameObject    FindChildByName( GameObject GO, string name ) {
            foreach ( GameObject child in Hierarchy.Children (GO) )
            {
                if(child.name == name )
                    return child;
            }
            return null;
        }

        public static GameObject    FindDescendantByName( GameObject GO, string name ){

            if(GO.name == name )
                return GO;
            foreach ( GameObject child in Hierarchy.Children (GO) )
            {
                GameObject recurse = FindDescendantByName(child, name );
                if( recurse != null)
                    return recurse;
            }
            return null;          
        }

        public static GameObject AddChild( GameObject GO, GameObject Adopted )
        {
            Adopted.transform.SetParent (GO.transform, true);
            return Adopted;
        }

        public static System.Collections.Generic.IEnumerable<GameObject> Ancestors( GameObject GO ) {
            GameObject g = GO;
            while (g != null) {
                yield return g;
                g = Hierarchy.Parent (g);
            }
        }
  
        public static List<GameObject> Ancestry( GameObject GO )
        {
            List<GameObject> ancestry = new List<GameObject>();

            GameObject g = GO;
            while (g != null) {
                ancestry.Add (g);
                g = Hierarchy.Parent (g);
            }

            return ancestry;
        }

        public static GameObject CommonAncestor( GameObject GO1, GameObject GO2 )
        {
            List<GameObject> list1 = Ancestry (GO1);
            List<GameObject> list2 = Ancestry (GO2);

            foreach (GameObject G1 in list1)
            {
                foreach (GameObject G2 in list2)
                {
                    if (G1 == G2)
                        return G1;
                }
            }

            return null;
        }


      
    }
}

//...or add to GameObject's methods through extensions
namespace UnityEngine.Extensions.Hierarchy
{
    public static class HierarchyExtensionsWrapper {
        //Relations
        public static IEnumerable<GameObject> Children ( this GameObject GO )
        {
            return UnityExtensions.Hierarchy.Children( GO );
        }

        public static GameObject    Parent( this GameObject GO )
        {
            return UnityExtensions.Hierarchy.Parent(GO);
        }

        public static GameObject    FirstChild( this GameObject GO )
        {
            return UnityExtensions.Hierarchy.FirstChild(GO);
        }

        public static GameObject    SecondChild( this GameObject GO )
        {
            return UnityExtensions.Hierarchy.SecondChild (GO);
        }

        public static GameObject    LastChild( this GameObject GO )
        {
            return UnityExtensions.Hierarchy.LastChild (GO);
        }

        //Find
        public static GameObject    FindChildByName( this GameObject GO, string name ) {
            return UnityExtensions.Hierarchy.FindChildByName (GO, name);
        }
    }
}

Personallly, I use SharpSerializer for my serialization in Unity. I’ve never tried to serialize GameObjects; I can see this approach serializing a lot of extraneous information.

SharpSerializer looks interesting, thank you. Can it handle Behaviours directly, though?

I’m not sure where you got the impression I was serializing GameObjects, or what you mean by extraneous information…