Strange Mesh.UserData Serialization Behavior

I’m trying to populate some userData in a Mesh, serialize the Mesh to JSON, and then deserialize it back into a Mesh and read back out the userData, using Rhino3dm (nuget 7.15.0). I kept not being able to find my userData object in the deserialized Mesh, and as I dig in, it appears it’s not even being added to the original mesh. Here is my code to reproduce:

using System;

namespace RhinoUserDataTest
{
    [System.Runtime.InteropServices.Guid("fbaca1a5-5282-41e0-b5ba-47566db455d9")]
    public class TestUserData : Rhino.DocObjects.Custom.UserData
    {
        public int TestNumber { get; set; }

        public TestUserData() { }

        public TestUserData(int testNumber)
        {
            TestNumber = testNumber;
        }

        public override string Description => "Test UserData";
 
        public override string ToString() => String.Format("testNumber={0}", TestNumber);
 
        protected override void OnDuplicate(Rhino.DocObjects.Custom.UserData source)
        {
            TestUserData src = source as TestUserData;
            if (src != null)
                TestNumber = src.TestNumber;
        }

        public override bool ShouldWrite => true;
 
        protected override bool Read(Rhino.FileIO.BinaryArchiveReader archive)
        {
            Rhino.Collections.ArchivableDictionary dict = archive.ReadDictionary();
            TestNumber = (int)dict[nameof(TestNumber)];
            return true;
        }
        protected override bool Write(Rhino.FileIO.BinaryArchiveWriter archive)
        {
            var dict = new Rhino.Collections.ArchivableDictionary(1, "TestUserData");
            dict.Set(nameof(TestNumber), TestNumber);
            archive.WriteDictionary(dict);
            return true;
        }
    }
    
    internal class Program
    {
        public static void Main(string[] args)
        {
            var originalMesh = new Rhino.Geometry.Mesh();
            originalMesh.Vertices.Add(0, 0, 0);
            originalMesh.Vertices.Add(1, 0, 0);
            originalMesh.Vertices.Add(1, 1, 0);
            originalMesh.Faces.AddFace(0, 1, 2);
            
            var tUD = new TestUserData(42);
            
            Console.WriteLine($"originalMesh.UserData.Count (before add): {originalMesh.UserData.Count}");
            originalMesh.UserData.Add(tUD);
            Console.WriteLine($"originalMesh.UserData.Count (after add): {originalMesh.UserData.Count}");

            var json = originalMesh.ToJSON(new Rhino.FileIO.SerializationOptions(){RhinoVersion = 7, WriteUserData = true});

            var deserializedMesh = Rhino.Geometry.Mesh.FromJSON(json);
            Console.WriteLine($"deserializedMesh.UserData.Count: {deserializedMesh.UserData.Count}");
            
        }
    }
}

When run, this outputs:

originalMesh.UserData.Count (before add): 0
originalMesh.UserData.Count (after add): 0
deserializedMesh.UserData.Count: 0

I tried stepping into Mesh.UserData.Add, which JetBrains Rider tells me looks like this:

 public bool Add(UserData userdata)
    {
      if (!(userdata is SharedUserDictionary))
      {
        Type type = userdata.GetType();
        ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
        if (!type.IsPublic || constructor == (ConstructorInfo) null)
          throw new ArgumentException("user-data must be a public class and have a parameterless constructor");
      }
      bool flag = UnsafeNativeMethods.ON_Object_AttachUserData(this.m_parent.ConstPointer(), userdata.NonConstPointer(true), true);
      if (flag)
        UserData.StoreInRuntimeList(userdata);
      return flag;
    }

When I step through that method, it claims to be following the logic path that leads to the ArgumentException being thrown, despite debugger telling me that type.IsPublic = true and constructor != null. However, the runtime does not actually bubble up an exception to trap. I’m guessing the source code I’m being shown is not the actual executing source…?

Any clues on what I’m doing wrong? Why would UserDataList.Add(…) fail on me? Is reading / writing UserData values from Rhino3dm not supported?

Is your custom UserData class in a plug-in assembly? I seem to remember that if it is not, the (de)serialization won’t work.

Aha! The UserData class was not defined inside a plugin assembly, but in a referenced assembly.

I didn’t realize this UserData layer was plugin-specific; I’ll shuffle the data elsewhere.

Thanks so much Menno!

Great, nice to know that was the solution.
Looks like this has been an issue for nine years :woozy_face:

UserData is built on top of ArchivableDictionary, you should find that works quite well! Just make keep in mind each object has two :blush:.

I two had to avoid using UserData because of this issue.