Create a tree of properties? My add-on acts upon a hierarchy of data (a tree). I am not sure how best to represent this hierarchy as properties to expose the data to the user.

For simplicity, let us define an example: say that each node of the tree has a "value" which for example purposes is a float. Each node also has zero, one, or many child nodes. As I understand it, properties must all be registered up front in register(), which is the problem -- I guess I need to register all the properties for all the children that could ever exist?

I have had good results with conditionally presenting UI elements in draw(), and so I thought that one approach could be to add an "isEnabled" flag to the node class and define the tree as having a certain maximum depth with each node having a maximum number of children. If in our example the maximum tree that we require has a max depth of 2 and max children of 5, we end up with 31 nodes: the root, the root's 5 children, and then each child has 5 children.

Then, for every node in the tree, for node.value and node.isEnabled register a property in register(). In draw, we can use the isEnabled flag to show or hide the children of a node, which kind of expands or collapses the tree. 

Is there a cleaner way to map this kind of data to Blender properties?

Edit:
I learned about PropertyGroup, but as I expected it doesn't like circular references.

class MyNode(bpy.types.PropertyGroup):
    value = bpy.props.IntProperty(name="value", default=1, min=1, max=1000, description="")
    child1 = bpy.props.PointerProperty(type=MyNode)


results in:

NameError: name 'MyNode' is not defined


But I think you could define classes for each level:

      A              MyRoot
   ___|___
  /       \
  B       C          MyTrunk
  |       |
 / \     / \
D   E   F   G        MyLeaf


I think that MyRoot can have a bpy.props.CollectionProperty(type=MyTrunk) and MyTrunk each have a collection of MyLeaf and then in register(), on single PointerProperty could be registered of type MyRoot.

This is pretty clean, except that you need to define a class for each level of the hierarchy. Therefore the hierarchy must necessarily be short and the maximum depth limited in advance (by the number of classes you're willing to define).
Leverage the collection

Collections in 2.8 have a hierarchy, well at least a children collection.


Result of running test script


Can add custom properties to the collection.
Collections do not have a parent property, since they do not follow
the single parent rule.  Have added one.
Tagged the collection as "node" by adding an instance only custom
property node["node"] 
Set the root node parent to scene collection for visibility sake,
similarly if the parent attribute is set to None


Test script.

import bpy
from bpy.props import FloatProperty, PointerProperty

# clean to test
for c in bpy.data.collections:
    if "node" in c.keys():
        bpy.data.collections.remove(c)


def link_children(self, context):
    if self.parent == self:
        self.parent = None
    if self.parent:
        print(self.name in self.parent.children)
        if self.name not in self.parent.children:
            self.parent.children.link(self)
    else:
        self.parent = context.scene.collection

    mynodes = [n for n in bpy.data.collections if "node" in n.keys()]
    for n in mynodes:

        for c in n.children:
            if c.parent is not n:
                n.children.unlink(c)
        # TODO remove any "dead roots" in scene collection 
    return None

float_prop = FloatProperty()
bpy.types.Collection.float_prop = float_prop
bpy.types.Collection.parent = PointerProperty(type=bpy.types.Collection, update=link_children)

def mynode(name, scene=bpy.context.scene):
    node = bpy.data.collections.new(name)
    node["node"] = True
    return node


scene = bpy.context.scene
my_root = mynode("My Root")
my_root.parent = scene.collection
my_root.float_prop = 1

for i in range(2):
    my_trunk = mynode(f"My Trunk {i}")
    my_trunk.float_prop = 2
    my_trunk.parent = my_root
    for j in range(2):
        my_leaf = mynode(f"My Leaf {i}.{j}")
        my_leaf.float_prop = 3
        my_leaf.parent = my_trunk    


def draw_node(self, context):
    col = context.collection
    if col and "node" in col.keys():
        layout.prop(col, "float_prop")


Drawing in the UI.

Unfortunately the outliner doesn't currently have a way to display properties.  Can draw in UI with something like this, here I've used the context.collection and if it is a "node" display the float property

def draw_node(self, context):
    col = context.collection
    if col and "node" in col.keys():
        layout.prop(col, "float_prop")


Extending:

Using old fashioned properties, or getters setters with bpy.props can extend this to have what appears to be its own API, for example


Creating an API with a similar feel

from collections.abc import Mapping
import bpy

class MyTreeNodes(Mapping):
    @property
    def _storage(self):
        return {n.name : MyTreeItem(n) for n in bpy.data.collections if "node" in n.keys()}

    def new(self, name):
        node = bpy.data.collections.new(name)
        node["node"] = True
        node.parent = self.id_data.collection
        return MyTreeItem(node)


    def __init__(self,  scene, *args, **kw):
        self.id_data = scene
        self._liststorage = list()
        #self._storage = dict(*args, **kw)

    def __getitem__(self, key):
        if isinstance(key, int):
            return list(self._storage.values())[key]
        return self._storage[key]

    def __iter__(self):
        return iter(self._storage)

    def __len__(self):
        return len(self._storage)

class MyTreeItem(bpy.types.Collection):
    def insert(self, name):
        pass

    def __init__(self, coll):
        self.is_node = "node" in self.keys()


def getnodes(self):
    return MyTreeNodes(self.id_data)

class NodeTree(bpy.types.PropertyGroup):
    nodes = property(getnodes)

bpy.utils.register_class(NodeTree)    
bpy.types.Scene.mytree = bpy.props.PointerProperty(type=NodeTree)


Code below pasted into python console, shown in image above

coll = C.collection
coll
mytree = C.scene.mytree
mytree
nodes = mytree.nodes
foo = nodes.new("foobar")
foo.parent = coll
nodes[0]
nodes['