This document describes the BSON serialization format for Mendix pages, including widget type mappings, required default properties, and common pitfalls.
The authoritative reference for BSON serialization is the reflection-data at:
reference/mendixmodellib/reflection-data/{version}-structures.json
Each structure entry contains:
qualifiedName: The API name (e.g.,Pages$DivContainer)storageName: The BSON$Typevalue (e.g.,Forms$DivContainer)defaultSettings: Required default property valuesproperties: Property definitions with types and requirements
Mendix uses different prefixes for API names vs storage names:
| API Prefix | Storage Prefix | Domain |
|---|---|---|
Pages$ |
Forms$ |
Page widgets |
Microflows$ |
Microflows$ |
Microflow elements |
DomainModels$ |
DomainModels$ |
Domain model elements |
Texts$ |
Texts$ |
Text/translation elements |
DataTypes$ |
DataTypes$ |
Data type definitions |
CustomWidgets$ |
CustomWidgets$ |
Pluggable widgets |
| Incorrect (will fail) | Correct Storage Name |
|---|---|
Forms$NoClientAction |
Forms$NoAction |
Forms$PageClientAction |
Forms$FormAction |
Forms$MicroflowClientAction |
Forms$MicroflowAction |
Pages$DivContainer |
Forms$DivContainer |
Pages$ActionButton |
Forms$ActionButton |
Each widget type requires specific default properties to be serialized. Studio Pro will fail to load the project if required properties are missing.
{
"$Type": "Forms$DivContainer",
"Appearance": { ... },
"ConditionalVisibilitySettings": null,
"Name": "",
"NativeAccessibilitySettings": null,
"OnClickAction": { "$Type": "Forms$NoAction", ... },
"RenderMode": "Div",
"ScreenReaderHidden": false,
"TabIndex": 0,
"Widgets": [3]
}{
"$Type": "Forms$LayoutGrid",
"Appearance": { ... },
"ConditionalVisibilitySettings": null,
"Name": "",
"Rows": [3],
"TabIndex": 0,
"Width": "FullWidth"
}{
"$Type": "Forms$LayoutGridRow",
"Appearance": { ... },
"Columns": [3],
"ConditionalVisibilitySettings": null,
"HorizontalAlignment": "None",
"SpacingBetweenColumns": true,
"VerticalAlignment": "None"
}{
"$Type": "Forms$LayoutGridColumn",
"Appearance": { ... },
"PhoneWeight": -1,
"PreviewWidth": -1,
"TabletWeight": -1,
"VerticalAlignment": "None",
"Weight": -1,
"Widgets": [3]
}{
"$Type": "Forms$ActionButton",
"Action": { ... },
"Appearance": { ... },
"AriaRole": "Button",
"ButtonStyle": "Default",
"CaptionTemplate": { ... },
"ConditionalVisibilitySettings": null,
"Icon": null,
"Name": "",
"NativeAccessibilitySettings": null,
"RenderType": "Button",
"TabIndex": 0,
"Tooltip": { ... }
}Note: Use RenderType (not RenderMode) for ActionButton.
{
"$Type": "Forms$DynamicText",
"Appearance": { ... },
"ConditionalVisibilitySettings": null,
"Content": { ... },
"Name": "",
"NativeAccessibilitySettings": null,
"NativeTextStyle": "Text",
"RenderMode": "Text",
"TabIndex": 0
}{
"$Type": "Forms$Text",
"Appearance": { ... },
"Caption": { ... },
"ConditionalVisibilitySettings": null,
"Name": "",
"NativeAccessibilitySettings": null,
"NativeTextStyle": "Text",
"RenderMode": "Text",
"TabIndex": 0
}{
"$Type": "Forms$Title",
"Appearance": { ... },
"Caption": { ... },
"ConditionalVisibilitySettings": null,
"Name": "",
"NativeAccessibilitySettings": null,
"TabIndex": 0
}{
"$Type": "Forms$DataView",
"Appearance": { ... },
"ConditionalEditabilitySettings": null,
"ConditionalVisibilitySettings": null,
"DataSource": { ... },
"Editability": "Always",
"FooterWidgets": [3],
"LabelWidth": 3,
"Name": "",
"NoEntityMessage": { ... },
"ReadOnlyStyle": "Control",
"ShowFooter": true,
"TabIndex": 0,
"Widgets": [3]
}Input widgets require several non-null properties for proper serialization:
{
"$Type": "Forms$TextBox",
"Appearance": { ... },
"AriaRequired": false,
"AttributeRef": {
"$ID": "<uuid>",
"$Type": "DomainModels$AttributeRef",
"Attribute": "Module.Entity.AttributeName",
"EntityRef": null
},
"AutoFocus": false,
"Autocomplete": true,
"AutocompletePurpose": "On",
"ConditionalEditabilitySettings": null,
"ConditionalVisibilitySettings": null,
"Editable": "Always",
"FormattingInfo": { ... },
"InputMask": "",
"IsPasswordBox": false,
"KeyboardType": "Default",
"LabelTemplate": { ... },
"MaxLengthCode": -1,
"Name": "textBox1",
"NativeAccessibilitySettings": null,
"OnChangeAction": { "$Type": "Forms$NoAction", ... },
"OnEnterAction": { "$Type": "Forms$NoAction", ... },
"OnEnterKeyPressAction": { "$Type": "Forms$NoAction", ... },
"OnLeaveAction": { "$Type": "Forms$NoAction", ... },
"PlaceholderTemplate": { ... },
"ReadOnlyStyle": "Inherit",
"ScreenReaderLabel": null,
"SourceVariable": null,
"SubmitBehaviour": "OnEndEditing",
"SubmitOnInputDelay": 300,
"TabIndex": 0,
"Validation": { ... }
}Required nested objects:
AttributeRef- Must haveAttributeas fully qualified path (e.g.,Module.Entity.AttributeName)FormattingInfo- Required for TextBox and DatePickerPlaceholderTemplate- RequiredForms$ClientTemplateobjectValidation- RequiredForms$WidgetValidationobject
The AttributeRef.Attribute field requires a fully qualified path in the format Module.Entity.AttributeName.
When using short attribute names in MDL (e.g., ATTRIBUTE 'Name'), the SDK automatically resolves them to fully qualified paths using the DataView's entity context:
Short: Name
Resolved: PgTest.Customer.Name
This resolution happens in cmd_pages_builder_input.go:resolveAttributePath() using the entity context set by the containing DataView.
| Action Type | Storage Name |
|---|---|
| No Action | Forms$NoAction |
| Save Changes | Forms$SaveChangesClientAction |
| Cancel Changes | Forms$CancelChangesClientAction |
| Close Page | Forms$ClosePageClientAction |
| Delete | Forms$DeleteClientAction |
| Show Page | Forms$FormAction |
| Call Microflow | Forms$MicroflowAction |
| Call Nanoflow | Forms$CallNanoflowClientAction |
Mendix BSON uses version markers for arrays:
| Marker | Meaning |
|---|---|
[3] |
Empty array |
[2, item1, item2, ...] |
Non-empty array with items |
[3, item1, item2, ...] |
Non-empty array (text items) |
Example:
"Widgets": [3] // Empty widgets array
"Widgets": [2, {...}] // One widget
"Items": [3, {...}] // One text translation itemStandard appearance object for widgets:
{
"$ID": "<uuid>",
"$Type": "Forms$Appearance",
"Class": "",
"DesignProperties": [3],
"DynamicClasses": "",
"Style": ""
}A page document has this top-level structure:
{
"$ID": "<uuid>",
"$Type": "Forms$Page",
"AllowedModuleRoles": [1],
"Appearance": { ... },
"Autofocus": "DesktopOnly",
"CanvasHeight": 600,
"CanvasWidth": 1200,
"Documentation": "",
"Excluded": false,
"ExportLevel": "Hidden",
"FormCall": { ... },
"Name": "PageName",
"Parameters": [3, ...],
"PopupCloseAction": "",
"Title": { ... },
"Url": "page_url",
"Variables": [3]
}Pluggable widgets like ComboBox use the CustomWidgets$CustomWidget type with a complex structure:
{
"$Type": "CustomWidgets$CustomWidget",
"Appearance": { ... },
"ConditionalEditabilitySettings": null,
"ConditionalVisibilitySettings": null,
"Editable": "Always",
"LabelTemplate": null,
"Name": "comboBox1",
"Object": {
"$Type": "CustomWidgets$WidgetObject",
"Properties": [2, { ... }],
"TypePointer": "<binary ID referencing ObjectType>"
},
"TabIndex": 0,
"Type": {
"$Type": "CustomWidgets$CustomWidgetType",
"HelpUrl": "...",
"ObjectType": {
"$Type": "CustomWidgets$WidgetObjectType",
"PropertyTypes": [2, { ... }]
},
"OfflineCapable": false,
"PluginWidget": false,
"WidgetId": "com.mendix.widget.web.combobox.Combobox"
}
}Each pluggable widget instance contains a full copy of both Type and Object:
| Component | BSON Size | Description |
|---|---|---|
| Type (CustomWidgetType) | ~54 KB | Widget definition with all PropertyTypes |
| Object (WidgetObject) | ~34 KB | Property values for all PropertyTypes |
| Total per widget | ~88 KB |
For a page with 4 ComboBox widgets: ~352 KB just for the widgets.
This is exactly how Mendix Studio Pro stores pluggable widgets - there is no deduplication within a page.
Critical: There are three levels of TypePointer references:
- WidgetObject.TypePointer → References
ObjectType.$ID(the WidgetObjectType) - WidgetProperty.TypePointer → References
PropertyType.$ID(the WidgetPropertyType) - WidgetValue.TypePointer → References
ValueType.$ID(the WidgetValueType)
CustomWidgetType
└── ObjectType (WidgetObjectType) ← WidgetObject.TypePointer
└── PropertyTypes[]
└── PropertyType (WidgetPropertyType) ← WidgetProperty.TypePointer
└── ValueType (WidgetValueType) ← WidgetValue.TypePointer
If any TypePointer is missing or references an invalid ID, you'll get errors like:
NullReferenceException in GenerateDefaultProperties(WidgetObject widgetObject)- Missing WidgetObject.TypePointerThe given key 'abc123...' was not present in the dictionary- Invalid PropertyType referenceCould not find widget property value for property X- Missing WidgetProperty for a PropertyType
To create pluggable widgets correctly, we use embedded templates extracted from working widgets:
sdk/widgets/
├── loader.go # Template loading and cloning
└── templates/
└── mendix-11.6/
├── combobox.json # Full ComboBox template (~5400 lines JSON)
├── datagrid.json # DataGrid template
└── ...
Each template contains:
type: The full CustomWidgetType definitionobject: The full WidgetObject with all property values
When creating a widget:
- Load the template from embedded JSON
- Clone both Type and Object with regenerated IDs
- Update the ID mapping so TypePointers reference the new IDs
- Modify specific property values (e.g.,
attributeEnumeration)
// Get template and clone with new IDs
embeddedType, embeddedObject, propertyIDs, objectTypeID, err := widgets.GetTemplateFullBSON(
pages.WidgetIDComboBox,
mpr.GenerateID,
)
// Update specific property value
updatedObject := updateWidgetPropertyValue(embeddedObject, propertyIDs, "attributeEnumeration", ...)const WidgetIDComboBox = "com.mendix.widget.web.combobox.Combobox"To extract a template from an existing widget in a Mendix project:
reader, _ := modelsdk.Open("project.mpr")
rawWidget, _ := reader.FindCustomWidgetType(pages.WidgetIDComboBox)
// rawWidget.RawType contains the CustomWidgetType
// rawWidget.RawObject contains the WidgetObject with all property valuesConvert to JSON and save to sdk/widgets/templates/mendix-{version}/.
This error means the $Type value is incorrect. Check the reflection-data for the correct storage name.
Examples:
Forms$NoClientAction→ UseForms$NoActionForms$PageClientAction→ UseForms$FormActionPages$DivContainer→ UseForms$DivContainer
The DataView's DataSource property is missing or incorrectly configured. A DataView using a page parameter needs a Forms$DataViewSource with proper EntityRef and SourceVariable:
{
"$Type": "Forms$DataViewSource",
"EntityRef": {
"$Type": "DomainModels$DirectEntityRef",
"Entity": "Module.EntityName"
},
"ForceFullObjects": false,
"SourceVariable": {
"$Type": "Forms$PageVariable",
"LocalVariable": "",
"PageParameter": "ParameterName",
"SnippetParameter": "",
"SubKey": "",
"UseAllPages": false,
"Widget": ""
}
}Common mistakes:
- Using
EntityPathSourceinstead ofDataViewSourcefor page parameters - Missing
EntityReforSourceVariableproperties PageParametershould be the parameter name without the$prefix
Widget properties are missing or have incorrect values. Check that all required default properties from the reflection-data are included.
Use this Python snippet to check widget default settings:
import json
with open('reference/mendixmodellib/reflection-data/11.0.0-structures.json') as f:
data = json.load(f)
# Find widget by API name
widget = data.get('Pages$DivContainer', {})
print('Storage name:', widget.get('storageName'))
print('Defaults:', json.dumps(widget.get('defaultSettings', {}), indent=2))
# Search by storage name
for key, val in data.items():
if val.get('storageName') == 'Forms$NoAction':
print(f'{key}: {val.get("defaultSettings")}')| File | Purpose |
|---|---|
sdk/mpr/writer_widgets.go |
Widget serialization to BSON |
sdk/mpr/writer_pages.go |
Page serialization |
sdk/mpr/reader_widgets.go |
Widget template extraction and cloning |
sdk/mpr/parser_page.go |
Page deserialization |
sdk/widgets/loader.go |
Embedded template loading |
sdk/widgets/templates/mendix-11.6/*.json |
Embedded widget templates |
sdk/pages/pages_widgets_advanced.go |
CustomWidget Go types |
mdl/executor/cmd_pages_builder_input.go |
Widget creation from MDL |
reference/mendixmodellib/reflection-data/*.json |
Type definitions |
Pluggable widgets like DataGrid2, ComboBox, and Gallery use a fundamentally different structure than built-in widgets.
CustomWidgets$CustomWidget
├── Type (CustomWidgets$CustomWidgetType)
│ ├── WidgetId: "com.mendix.widget.web.datagrid.Datagrid"
│ ├── ObjectType (CustomWidgets$WidgetObjectType)
│ │ └── PropertyTypes[] (CustomWidgets$WidgetPropertyType)
│ │ ├── $ID: "<property-type-id>"
│ │ ├── PropertyKey: "datasource"
│ │ └── ValueType (CustomWidgets$WidgetValueType)
│ └── ...
└── Object (CustomWidgets$WidgetObject)
└── Properties[] (CustomWidgets$WidgetProperty)
├── TypePointer: "<property-type-id>" // References PropertyTypes.$ID
└── Value (CustomWidgets$WidgetValue)
├── TypePointer: "<value-type-id>"
├── DataSource, AttributeRef, PrimitiveValue, TextTemplate, etc.
└── Objects[] (for nested object lists like columns)
-
Type-Object ID Consistency:
Object.Properties[].TypePointerMUST reference validType.ObjectType.PropertyTypes[].$IDvalues. When regenerating IDs, both must use the same ID mapping. -
All Properties Required: Every PropertyType in the Type must have a corresponding WidgetProperty in the Object. Missing properties cause "widget definition has changed" errors.
-
TextTemplate Properties: Properties with
ValueType.Type = "TextTemplate"need properForms$ClientTemplatestructures, not null:"TextTemplate": { "$ID": "<uuid>", "$Type": "Forms$ClientTemplate", "Fallback": { "$ID": "<uuid>", "$Type": "Texts$Text", "Items": [] }, "Parameters": [], "Template": { "$ID": "<uuid>", "$Type": "Texts$Text", "Items": [] } }
CRITICAL: Empty arrays must be
[], NOT[2]. In JSON,[2]is an array containing the integer 2, not an empty array with a version marker. The version markers only exist in BSON format, not in the JSON templates. -
Default Values from Template: Use embedded templates from
sdk/widgets/templates/which include both Type AND Object with correct default values.
// Load template with both Type and Object
embeddedType, embeddedObject, propertyTypeIDs, objectTypeID, err :=
widgets.GetTemplateFullBSON(widgetID, mpr.GenerateID)
// Update the template Object with specific values (datasource, columns)
rawObject := updateTemplateObject(embeddedObject, propertyTypeIDs, datasource, columns)
// Create widget with cloned Type and updated Object
widget := &pages.CustomWidget{
RawType: embeddedType,
RawObject: rawObject,
...
}Critical Insight: Pluggable widgets often contain nested WidgetObject instances (e.g., DataGrid2 columns). These nested objects must follow the same completeness rule as the parent widget:
ALL properties defined in the nested
ObjectType.PropertyTypesmust be created with default values, not just the ones with explicit values.
The DataGrid2 columns property has an ObjectType with 21 PropertyTypes:
showContentAs, attribute, content, dynamicText, exportValue, header, tooltip,
filter, visible, sortable, resizable, draggable, hidable, allowEventPropagation,
width, minWidth, minWidthLimit, size, alignment, columnClass, wrapText
Wrong approach (creates incomplete columns):
// Only creates 5 properties - columns won't appear in Page Explorer
columnProperties := bson.A{int32(2),
buildProperty("showContentAs", "attribute"),
buildProperty("attribute", attrPath),
buildProperty("header", headerText),
buildProperty("content", filterWidget),
buildProperty("filter", filterWidget),
}Correct approach (creates all 21 properties):
// Create ALL properties from the template's ObjectType.PropertyTypes
for _, propType := range columnObjectType.PropertyTypes {
if propType.PropertyKey == "attribute" {
// Use explicit value
columnProperties = append(columnProperties, buildProperty(propType, attrPath))
} else {
// Use default value from template
columnProperties = append(columnProperties, buildDefaultProperty(propType))
}
}| Symptom | Cause |
|---|---|
| Columns not visible in Page Explorer | Column objects missing properties |
| "widget definition has changed" error | Property count mismatch |
| Widget shows in editor but not explorer | Partial object recognition |
The embedded templates at sdk/widgets/templates/mendix-11.6/datagrid.json contain the full column ObjectType definition:
{
"PropertyKey": "columns",
"ValueType": {
"ObjectType": {
"PropertyTypes": [
{ "PropertyKey": "showContentAs", "ValueType": { "DefaultValue": "attribute" } },
{ "PropertyKey": "attribute", ... },
{ "PropertyKey": "content", ... },
// ... all 21 properties with their default values
]
}
}
}When creating columns, iterate through ALL PropertyTypes and create a WidgetProperty for each one.
Critical Insight: Properties with ValueType.Type = "Expression" (like visible, editable, etc.) require a non-empty Expression value. Template widgets often have empty/placeholder Expression values that will cause validation errors if cloned directly.
The visible property on DataGrid2 columns controls whether the column is displayed. It uses the Expression type:
{
"$Type": "CustomWidgets$WidgetProperty",
"TypePointer": "<visible-property-type-id>",
"Value": {
"$Type": "CustomWidgets$WidgetValue",
"Expression": "true", // REQUIRED: Non-empty expression
"TypePointer": "<visible-value-type-id>",
...
}
}Template pitfall: The template's visible property may have "Expression": "" (empty). When cloning column properties, you must:
- Check if Expression is empty: If the template has an empty Expression
- Rebuild the property: Create a new property with
Expression: "true"instead of cloning
// In cloneAndUpdateColumnProperties
if propKey == "visible" {
var hasExpression bool
for _, pe := range propMap {
if pe.Key == "Value" {
if valDoc, ok := pe.Value.(bson.D); ok {
for _, ve := range valDoc {
if ve.Key == "Expression" && ve.Value != "" {
hasExpression = true
}
}
}
}
}
if !hasExpression {
// Rebuild with Expression: "true" instead of cloning empty value
result = append(result, pb.buildColumnExpressionProperty(visibleEntry, "true"))
} else {
result = append(result, pb.clonePropertyWithNewIDs(propMap))
}
}| Error | Property | Solution |
|---|---|---|
| CE0642 "Property 'Visible' is required" | Column visible |
Rebuild with Expression: "true" |
| CE0642 "Property 'Editable' is required" | Column editable |
Rebuild with Expression: "true" |
| Column always hidden | Column visible |
Check Expression isn't empty |
| Error | Cause | Solution |
|---|---|---|
| CE0463 "widget definition has changed" | Object properties don't match Type PropertyTypes | Use template Object as base, only modify needed properties |
| CE0642 "Property 'X' is required" | Expression-type property has empty Expression value | Rebuild property with non-empty Expression (e.g., "true") |
| "Sequence contains no matching element" | Missing properties in Object | Ensure all PropertyTypes have corresponding WidgetProperties |
| Column captions empty | TextTemplate not properly structured | Use Forms$ClientTemplate with Fallback and Template |
| Columns not in Page Explorer | Nested WidgetObjects incomplete | Create ALL properties from ObjectType.PropertyTypes |
When cloning pluggable widget properties from a template:
- Clone everything with new IDs - All
$IDvalues must be regenerated - Keep TypePointers consistent - Don't regenerate TypePointer values (they reference the Type)
- Handle empty Expression values - Rebuild Expression-type properties if the template has empty values
- Add missing required properties - If the template is sparse, add required properties that are missing
The CE0463 error ("The definition of this widget has changed") is one of the most subtle errors when building pluggable widgets programmatically. This section documents the findings from systematic debugging of DataGrid2 custom content columns.
CE0463 is not simply about the Type section being outdated. It triggers when the Object property values are inconsistent with the current mode of the widget.
Pluggable widgets have mode-switching properties (like DataGrid2's showContentAs)
that change which other properties are visible vs hidden. The widget's editorConfig.js
(inside the .mpk package) defines these visibility rules:
// From Datagrid.editorConfig.js (deminified):
// When NOT customContent mode: HIDE content, allowEventPropagation, exportValue
"customContent" !== col.showContentAs &&
hideNestedPropertiesIn(properties, values, "columns", idx,
["content", "allowEventPropagation", "exportValue"]);
// When IN customContent mode: HIDE tooltip
"customContent" === col.showContentAs &&
hidePropertyIn(properties, values, "columns", idx, "tooltip");When showContentAs changes, certain properties must have mode-appropriate values:
| Property | attribute mode | customContent mode |
|---|---|---|
showContentAs |
PV="attribute" | PV="customContent" |
attribute |
AttrRef=present | AttrRef=null |
content |
HIDDEN (W=0) | VISIBLE (W=n widgets) |
tooltip |
VISIBLE (TT=present) | HIDDEN (TT=null) |
exportValue |
HIDDEN (no TT) | VISIBLE (TT=present) |
allowEventPropagation |
HIDDEN (PV=true) | VISIBLE (PV=true, required) |
dynamicText |
VISIBLE | HIDDEN |
Key insight: Simply cloning a template column (which has attribute-mode defaults) and
changing only showContentAs to customContent triggers CE0463 because the hidden/visible
property states are still in attribute mode.
Taking a working widget (P012 ProductGrid with attribute-mode columns, passes CE0463 after
mx update-widgets) and changing only the PrimitiveValue of showContentAs from
"attribute" to "customContent" immediately triggers CE0463 — proving the issue is about
Object property state consistency, not Type section correctness.
The project's widgets/ folder contains .mpk files (ZIP archives) for each pluggable
widget. The canonical widget definition is in {WidgetName}.xml inside the mpk:
widgets/com.mendix.widget.web.Datagrid.mpk
├── Datagrid.xml ← Widget schema: properties, types, defaults, enums
├── Datagrid.editorConfig.js ← Property visibility rules (mode-dependent hiding)
├── package.xml ← Package metadata and version
└── com/mendix/.../Datagrid.js ← Runtime widget code
The Datagrid.xml defines all 21 column properties with their types, defaults, and
constraints. The editorConfig.js defines which properties are visible/hidden based on
the current values of other properties. Together they form the complete specification
that mx update-widgets uses to normalize widget Objects.
Running mx update-widgets after creating pages normalizes all widget Objects to match
the mpk definition. This eliminates CE0463 regardless of what property states the
programmatic builder set:
# After creating pages with mxcli:
reference/mxbuild/modeler/mx update-widgets /path/to/app.mprThis is safe: it only updates the Object section (not the Type), and only changes properties that are in an inconsistent state.
To avoid CE0463 without post-processing, the column builder must adjust properties based on the showContentAs mode. When building a customContent column:
- Set
showContentAsPV to "customContent" - Set
contentWidgets to actual content widgets - Clear
tooltipTextTemplate (hidden in customContent mode) - Ensure
exportValuehas a TextTemplate (visible in customContent mode) - Keep
allowEventPropagationas-is from template (visible in customContent mode; clearing it triggers CE0642 "Property 'Allow row events' is required") - Clear
attributeAttrRef (no attribute in customContent mode)