Serializing complex classes

Hello everyone,

What’s your preferred way of serializing complex, and nested classes in late 2023? The 5 year old sample by @dale uses the BinaryFormatter which is now discontinued for security reasons.

Which alternatives are you using?

.NET offers several in-box serializers that can handle untrusted data safely:

Also, doing the user data thing is a great alternative - the Rhino way.

— Dale

1 Like

Thanks for your prompt response, Dale!

Can you elaborate? If you are referring to ArchivableDictionaries attached to geometry, then I’m doing this already, but there are some plugin-level settings I need to serialize too.

We are quite happy using Json.NET - Newtonsoft.
It is solid, fast and has every edge case and feature we’ve needed.

1 Like

Thanks @david.birch.uk!

I ended up going with this base class definition which so far seems to be covering all my needs. Any feedback would be much appreciated.

using System;
using System.Reflection;
using Newtonsoft.Json;
using Rhino.FileIO;

namespace UI
{
    /// <summary>
    /// Represents a base class for serializable objects, providing methods for serialization and deserialization.
    /// </summary>
    [Serializable]
    public abstract class SerializableBase
    {
        /// <summary>
        /// Copies property values from another instance of SerializableBase to this instance.
        /// </summary>
        /// <param name="src">The source instance from which to copy properties.</param>
        public virtual void Create(SerializableBase src)
        {
            PropertyInfo[] properties = src.GetType().GetProperties();
            foreach (PropertyInfo propertyInfo in properties)
            {
                if (propertyInfo.CanRead && propertyInfo.CanWrite)
                {
                    propertyInfo.SetValue(this, propertyInfo.GetValue(src, null), null);
                }
            }
        }

        public virtual bool IsValid => true;

        /// <summary>
        /// Writes the current object to a binary archive.
        /// </summary>
        /// <param name="archive">The binary archive writer to which the object is written.</param>
        /// <returns>true if the write operation is successful; otherwise, false.</returns>
        public bool Write(BinaryArchiveWriter archive)
        {
            bool result = false;
            if (archive != null)
            {
                try
                {
                    archive.Write3dmChunkVersion(1, 0);
                    string json = JsonConvert.SerializeObject(this, Formatting.None, new JsonSerializerSettings
                    {
                        TypeNameHandling = TypeNameHandling.All
                    });
                    archive.WriteString(json);
                    result = !archive.WriteErrorOccured;
                }
                catch
                {
                }
            }

            return result;
        }

        /// <summary>
        /// Reads and deserializes an object from a binary archive.
        /// </summary>
        /// <param name="archive">The binary archive reader from which the object is read.</param>
        /// <returns>true if the read operation is successful; otherwise, false.</returns>
        public bool Read(BinaryArchiveReader archive)
        {
            bool result = false;
            if (archive != null)
            {
                archive.Read3dmChunkVersion(out var major, out var minor);
                if (1 == major && minor == 0)
                {
                    try
                    {
                        string json = archive.ReadString();
                        SerializableBase src = JsonConvert.DeserializeObject<SerializableBase>(json, new JsonSerializerSettings
                        {
                            TypeNameHandling = TypeNameHandling.Auto,
                            ObjectCreationHandling = ObjectCreationHandling.Replace
                        });
                        Create(src);
                        result = !archive.ReadErrorOccured;
                    }
                    catch
                    {
                    }
                }
            }

            return result;
        }
    }
}

That is good news :grinning: @mrhe

Code looks good, couple of thoughts:

  1. Probably worth putting some logging in the Catch Blocks & setting result = false?
  2. If you are serializing many objects you might consider moving your JsonSerializerSettings to a static field to avoid repeated object creation.
  3. It would depend on how you use Create and i am not sure of your use case, if you are using at as a Clone method then you may need to be careful about List properties which would then become shared between the src and the new copy. This is the shallow / deep clone issue and there are lots of places to read up on it, i tend to avoid it by tripping via json when creating a clone, its slower but it works.

Hope that helps

1 Like

Thanks for the pointers @david.birch.uk!

Here is an updated version for whoever is interested in using it:

using System;
using System.Diagnostics;
using System.Reflection;
using Newtonsoft.Json;
using Rhino.FileIO;

namespace UI
{
    /// <summary>
    /// Represents a base class for serializable objects, providing methods for serialization and deserialization.
    /// </summary>
    [Serializable]
    public abstract class SerializableBase
    {
        /// <summary>
        /// Static JsonSerializerSettings used for serializing and deserializing objects. 
        /// This configuration helps in managing the type handling and object creation process during serialization.
        /// </summary>
        /// <remarks>
        /// - TypeNameHandling.Auto: Automatically handles the type information of the serialized objects, 
        ///   ensuring the correct types are instantiated during deserialization.
        /// - ObjectCreationHandling.Replace: Instructs the serializer to replace objects in collections 
        ///   and properties rather than merging when deserializing.
        /// </remarks>
        private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto,
            ObjectCreationHandling = ObjectCreationHandling.Replace
        };

        /// <summary>
        /// Copies property values from another instance of SerializableBase to this instance.
        /// </summary>
        /// <param name="src">The source instance from which to copy properties.</param>
        public virtual void Create(SerializableBase src)
        {
            PropertyInfo[] properties = src.GetType().GetProperties();
            foreach (PropertyInfo propertyInfo in properties)
            {
                if (propertyInfo.CanRead && propertyInfo.CanWrite)
                {
                    propertyInfo.SetValue(this, propertyInfo.GetValue(src, null), null);
                }
            }
        }

        public virtual bool IsValid => true;

        /// <summary>
        /// Writes the current object to a binary archive.
        /// </summary>
        /// <param name="archive">The binary archive writer to which the object is written.</param>
        /// <returns>true if the write operation is successful; otherwise, false.</returns>
        public bool Write(BinaryArchiveWriter archive)
        {
            bool result = false;
            if (archive != null)
            {
                try
                {
                    archive.Write3dmChunkVersion(1, 0);
                    string json = JsonConvert.SerializeObject(this, Formatting.None, SerializerSettings);
                    archive.WriteString(json);
                    result = !archive.WriteErrorOccured;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"Write operation failed: {ex.Message}");
                    result = false;
                }
            }
            return result;
        }

        /// <summary>
        /// Reads and deserializes an object from a binary archive.
        /// </summary>
        /// <param name="archive">The binary archive reader from which the object is read.</param>
        /// <returns>true if the read operation is successful; otherwise, false.</returns>
        public bool Read(BinaryArchiveReader archive)
        {
            bool result = false;
            if (archive != null)
            {
                archive.Read3dmChunkVersion(out var major, out var minor);
                if (1 == major && minor == 0)
                {
                    try
                    {
                        string json = archive.ReadString();
                        SerializableBase src = JsonConvert.DeserializeObject<SerializableBase>(json, SerializerSettings);
                        Create(src);
                        result = !archive.ReadErrorOccured;
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine($"Read operation failed: {ex.Message}");
                        result = false;
                    }
                }
            }
            return result;
        }
    }
}
2 Likes