DataObject (BIM elements), Collection (hierarchy), and Base (foundation type). Connector-published walls, doors, and rooms are usually DataObject instances with BIM fields in properties.
When to read this: before filtering or exporting model data. Read next: Find objects by property for filter recipes, or BIM data patterns for connector data structure.
For filtering and indexing recipes, see Find objects by property. Bootstrap: Automate with scripts.
Find doors and walls in received data
AfterReceive2, use Flatten() and read DataObject.properties:
Flatten() on Base. The BIM fields live in properties, not in typed Door classes.
DataObject and Collection
Most Speckle connectors emitDataObject (Speckle.Objects.Data) — name, properties, and displayValue — rather than bespoke typed classes. Use Collection (Speckle.Sdk.Models.Collections) to organize hierarchies. Plain Base or custom subclasses still work for self-contained apps; prefer DataObject/Collection when interoperating with connectors and the viewer. See Object Schema for field definitions.
The Base Class
Everything sent to or received from Speckle inherits fromBase. It supports both static (typed C# properties) and dynamic (runtime, string-keyed) properties on the same object.
Objects are content-addressed: once sent, an object’s
id is a hash of its serialized content. Identical content always produces the same id.Static vs Dynamic Properties
Typed geometry classes (fromSpeckle.Objects) declare static properties with full C# type support, but every Base-derived type also accepts arbitrary dynamic properties:
Base with the indexer when the shape is unknown or ad hoc. Both produce the same wire format.
Property names starting with
__ are ignored entirely (not hashed, not serialized). Property names cannot contain . or /. Avoid dynamic names starting with _ or @ unless you intend to detach that property — a double @@ prefix or invalid characters throw InvalidPropNameException.Complete Example
The
Room example teaches Base mechanics directly. For real BIM-like data, model with DataObject and Collection instead — the same detachment and introspection rules apply.Discovering Properties at Runtime
UseGetMembers() (typed + dynamic), GetDynamicMemberNames(), or DynamicPropertyKeys. Safe access: obj["material"] as string ?? "Unknown", obj.TryGetName(), obj.TryGetDisplayValue().
GetDetachedProp/SetDetachedProp extension methods read or write detached properties without knowing whether they’re typed or dynamic.
Detachment
By default, nestedBase objects serialize inline with their parent. Detachment stores a nested object separately and keeps a reference — useful for large or frequently-shared sub-graphs.
- Dynamic (@ prefix)
- Typed ([DetachProperty])
Chunking
Pair[Chunkable] with [DetachProperty] to split very large lists across multiple stored objects:
[Chunkable] is honored by Serialize/Send/Send2. The connector-scale SendPipeline path (see Publish large models (connectors)) detaches on [DetachProperty] but does not currently act on [Chunkable].Custom Types
Before defining a fully custom type, consider whetherDataObject already fits. Reach for a custom type when you want compile-time-checked properties, or when the object is internal to your own application.
Display Values
When you send data and expect it in the 3D viewer, the object must expose adisplayValue property containing renderable geometry. Schema details: Object Schema: displayValue.
The recommended container is DataObject:
displayValue dynamically on plain Base for quick scripts. For a root Base with nested Point/Line/Mesh properties (as in the Quickstart), the viewer can render that geometry without an explicit displayValue. Connector-style DataObject graphs need an explicit displayValue — bare primitives at the root of the version object often won’t render.
Use BaseExtensions after receive:
displayValue. PascalCase DisplayValue serializes under a different key and the viewer will not recognize it.
Traversing the Object Graph
The simplest walk isBaseExtensions.Flatten or Traverse:
Flatten follows Base values and list values. For dictionaries it only inspects keys — a Dictionary<string, Base> will not have its values traversed.
Rule-Based Traversal
For connector-style traversal, useGraphTraversal with DefaultTraversal:
DefaultTraversal understands elements/@elements, displayValue/@displayValue, and definition/@definition.
Platform traversal patterns: Traversal Recipes. .NET filter/index recipes: Find objects by property.
FAQ and Pitfalls
Why did my custom type deserialize as plain Base?
Why did my custom type deserialize as plain Base?
The type’s assembly was not passed to
AddSpeckleSdk. Register it in the assemblies argument — see Dependency injection (connectors).When should I use Flatten vs GraphTraversal?
When should I use Flatten vs GraphTraversal?
Use Flatten for simple filtering or indexing on small-to-medium graphs. Use GraphTraversal with
DefaultTraversal when you need connector-style rules on large BIM graphs.Sent an object but nothing shows in the viewer
Sent an object but nothing shows in the viewer
Check that the object (or one of its
elements) has a displayValue containing Mesh, Line, or Point geometry.InvalidPropNameException when setting a dynamic property
InvalidPropNameException when setting a dynamic property
The key is empty, starts with
@@, or contains .//. Use IsPropNameValid to check before setting if the key comes from untrusted input.obj.id is null right after construction
obj.id is null right after construction
id is only populated after send or deserialization — it’s a content hash, not something you set.Detached property comes back null after receive
Detached property comes back null after receive
Confirm the property was marked with
[DetachProperty] (typed) or an @ prefix (dynamic) before sending.Next Steps
Core concepts
Projects, models, and version lifecycle
Find objects by property
Flatten, filter, and index recipes
Operations API
Send and receive object graphs