How to Serialize Custom Data in FreeCAD

This article provides a practical guide on serializing and deserializing custom data structures within FreeCAD. You will learn how to leverage FreeCAD’s native property framework, implement state restoration using Python proxy objects via __getstate__ and __setstate__, and use C++ Save and Restore routines for low-level property implementations.


Method 1: Using Built-in FreeCAD Properties

The most robust way to manage custom data in FreeCAD is to use native App::Property types. Native properties automatically handle serialization into the FreeCAD XML/document archive (.FCStd) when saved and restored.

If your data can be expressed as primitive structures (lists, dictionaries, or strings), store it as a JSON-encoded string within an App::PropertyString:

import json
import FreeCAD as App

# Create a generic feature object
obj = App.ActiveDocument.addObject("App::FeaturePython", "CustomDataHolder")

# Add a string property to hold serialized data
obj.addProperty("App::PropertyString", "CustomPayload", "Data", "Stores serialized JSON")

# Serialization
data_to_store = {"node_ids": [101, 102, 103], "tolerance": 0.005, "enabled": True}
obj.CustomPayload = json.dumps(data_to_store)

# Deserialization
retrieved_data = json.loads(obj.CustomPayload)
print(retrieved_data["tolerance"])

Method 2: Python Proxy State (__getstate__ and __setstate__)

When building scripted objects (FeaturePython), non-property attributes attached to your Python proxy class are not saved by default. FreeCAD uses Python's standard pickle mechanism via __getstate__ and __setstate__ to persist internal proxy attributes.

import FreeCAD as App

class CustomFeature:
    def __init__(self, obj):
        obj.Proxy = self
        self.internal_matrix = [[1, 0], [0, 1]]
        self.version = 1.0

    def execute(self, obj):
        pass

    def __getstate__(self):
        """Serialize custom proxy variables into a dictionary/tuple."""
        return {
            "internal_matrix": self.internal_matrix,
            "version": self.version
        }

    def __setstate__(self, state):
        """Deserialize and restore state when the document is opened."""
        self.internal_matrix = state.get("internal_matrix", [])
        self.version = state.get("version", 1.0)

# Instantiation
doc = App.ActiveDocument
feature = doc.addObject("App::FeaturePython", "StatefulObject")
CustomFeature(feature)
doc.recompute()

When saving the document, FreeCAD calls __getstate__ and writes the resulting byte stream to the .FCStd container. When reopening, it instantiates the class and calls __setstate__.


Method 3: Handling Large Binary Structures (External Document Files)

If you have large binary payloads (such as raw point clouds or mesh arrays) that should not inflate the main Document.xml file, you can hook into FreeCAD's document stream writers.

In Python scripted objects, implement SaveDocFile and RestoreDocFile:

class BinaryDataFeature:
    def __init__(self, obj):
        obj.Proxy = self
        self.raw_bytes = b"\x00\x01\x02\x03\x04"

    def SaveDocFile(self, file_path):
        """Called by FreeCAD during document save. Writes to an auxiliary file in the archive."""
        with open(file_path, "wb") as f:
            f.write(self.raw_bytes)

    def RestoreDocFile(self, file_path):
        """Called by FreeCAD during document load."""
        with open(file_path, "rb") as f:
            self.raw_bytes = f.read()

Method 4: C++ Custom Property Implementation

When developing compiled C++ extensions, you inherit from App::Property and override the XML read/write methods:

#include <App/Property.h>
#include <Base/Writer.h>
#include <Base/XMLReader.h>

class PropertyCustomData : public App::Property
{
    TYPESYSTEM_HEADER();

public:
    std::vector<int> values;

    void Save(Base::Writer &writer) const override {
        writer.Stream() << writer.ind() << "<CustomData count=\"" << values.size() << "\">";
        for (int v : values) {
            writer.Stream() << v << " ";
        }
        writer.Stream() << "</CustomData>\n";
    }

    void Restore(Base::XMLReader &reader) override {
        reader.readElement("CustomData");
        int count = reader.getAttributeAsInteger("count");
        values.clear();
        values.reserve(count);
        for (int i = 0; i < count; ++i) {
            int val;
            reader >> val;
            values.push_back(val);
        }
        reader.readEndElement("CustomData");
    }
};

This ensures your C++ data types integrate directly with FreeCAD's XML parser and schema validator.