JSON Serialization Migration Guide
Serialization is about saving the state of your workspace so that it can be loaded back into a workspace later. This includes serializing the state of any blocks, variables, or plugins that you want to round-trip.
Originally Blockly only provided an XML-based serialization system, but now it also includes a JSON-based system. The XML system is being iceboxed (meaning it won’t receive new features) but the JSON system will continue to improve.
This document outlines how to migrate your project from the old XML system to the new JSON system. To learn more about how JSON serialization works, visit the serialization guide. This document also outlines the migration in the order that you should perform it. For instance, you could end up with corrupted saves if you migrate your workspace without migrating blocks.
Note that migration is completely optional. The XML system is being iceboxed, not deprecated, which means it will continue to work for the foreseeable future. Migration is only necessary if you want to get the latest-and-greatest features!
Problems
There is one main problem with migrating to the new system: backwards compatibility.
If you are currently storing XML-formatted save files, you need to make sure that those files can still be loaded into a workspace. Be careful never to remove something that loads old XML data, unless you’ve converted all your old XML saves to JSON.
Upgrading blocks
The XML system used the mutationToDom and domToMutation functions to
serialize the extra state of blocks. The JSON system uses saveExtraState and
loadExtraState instead.
Here is the block definition we will be using as an example:
Blockly.Blocks['lists_create_with'] = {
init: function() { /* ... */ },
mutationToDom: function() {
var container = Blockly.utils.xml.createElement('mutation');
container.setAttribute('items', this.itemCount_);
return container;
},
domToMutation: function(xmlElement) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
this.updateShape_();
},
// etc...
};
-
Add default
saveExtraStateandloadExtraStatedefinitions to wherever yourmutationToDomanddomToMutationfunctions are defined. In this case, this is the block, but if you have a separate mutator that definesmutationToDomanddomToMutationyou should also put the JSON serialization functions there.These implementations are just wrappers of the XML serialization and reflect what Blockly already does behind the scenes. You should move on to the next step in order to fully migrate.
Blockly.Blocks['lists_create_with'] = {
init: function() { /* ... */ },
mutationToDom: function() {
var container = Blockly.utils.xml.createElement('mutation');
container.setAttribute('items', this.itemCount_);
return container;
},
domToMutation: function(xmlElement) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
this.updateShape_();
},
// Add these functions:
saveExtraState: function() {
return Blockly.Xml.domToText(this.mutationToDom());
},
loadExtraState: function(state) {
this.domToMutation(Blockly.utils.xml.textToDom(state));
},
// etc...
};
- Modify
saveExtraStateandloadExtraStateto return the state directly.
See the Extensions and Mutators documentation for info on what this should look like.
Blockly.Blocks['lists_create_with'] = {
init: function() { /* ... */ },
mutationToDom: function() {
var container = Blockly.utils.xml.createElement('mutation');
container.setAttribute('items', this.itemCount_);
return container;
},
domToMutation: function(xmlElement) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
this.updateShape_();
},
saveExtraState: function() {
return {'itemCount': this.itemCount_};
},
loadExtraState: function(state) {
this.itemCount_ = state['itemCount'];
this.updateShape_();
},
// etc...
};
-
Optional: Remove the
mutationToDomfunction, but leavedomToMutation.There are some circumstances in which you should not remove
mutationToDom.When to keepmutationToDom:- Your mutator is registered with
Blockly.Extensions.registerMutator, which requiresmutationToDomif you havedomToMutation. - You call
Blockly.Procedures.mutateCallerswhich relies onmutationToDomto keep procedure blocks in sync. - Your project still writes XML elsewhere.
If you're not sure, just leave
mutationToDomin your code. You should leavedomToMutationregardless of whether or not you removemutationToDomso that you can load old saves. - Your mutator is registered with
Upgrading Fields
The XML system used the toXml and fromXml functions to serialize the state
of fields. The JSON system uses saveState and loadState instead.
Here is the code we will be using as an example:
CustomFields.FieldMap.prototype.toXml = function(fieldElement) {
fieldElement.textContent = this.getValue();
fieldElement.setAttribute('zoom', this.getZoomLevel());
return fieldElement;
};
CustomFields.FieldMap.prototype.fromXml = function(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
- Add default
saveStateandloadStatedefinitions to your fields, which just call yourtoXmlandfromXmlfunctions. Again, these implementations are essentially just wrappers of the XML serialization. You should move on to the next step in order to fully migrate.
CustomFields.FieldMap.prototype.toXml = function(fieldElement) {
fieldElement.textContent = this.getValue();
fieldElement.setAttribute('zoom', this.getZoomLevel());
return fieldElement;
};
CustomFields.FieldMap.prototype.fromXml = function(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
// Add these functions:
CustomFields.FieldMap.prototype.saveState = function() {
var elem = Blockly.utils.xml.createElement("field");
elem.setAttribute("name", this.name || '');
return Blockly.Xml.domToText(this.toXml(elem));
};
CustomFields.FieldMap.prototype.loadState = function(state) {
this.fromXml(Blockly.utils.xml.textToDom(state));
};
- Modify the
saveStateandloadStatefunctions to return the state directly.
See the Creating a custom field documentation for info on what this should look like.
CustomFields.FieldMap.prototype.toXml = function(fieldElement) {
fieldElement.textContent = this.getValue();
fieldElement.setAttribute('zoom', this.getZoomLevel());
return fieldElement;
};
CustomFields.FieldMap.prototype.fromXml = function(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
CustomFields.FieldMap.prototype.saveState = function() {
return {
'country': this.getValue(),
'zoom': this.getZoomLevel(),
};
};
CustomFields.FieldMap.prototype.loadState = function(state) {
this.setValue(state['country']);
this.setZoomLevel(state['zoom']);
};
- Remove the
toXmlfunctions, but leavefromXml.
It is important to leavefromXmlso that you can load old saves, buttoXmlis unnecessary if you won’t ever be saving to XML.
CustomFields.FieldMap.prototype.fromXml = function(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
CustomFields.FieldMap.prototype.saveState = function() {
return {
'country': this.getValue(),
'zoom': this.getZoomLevel(),
};
};
CustomFields.FieldMap.prototype.loadState = function(state) {
this.setValue(state['country']);
this.setZoomLevel(state['zoom']);
};
Upgrading toolboxes
If you want to use the new JSON hooks for blocks and fields, you will have to specify your toolbox using JSON as well.
-
Read the toolbox documentation to understand how a JSON toolbox is structured.
-
Run the following code in your browser’s console.
This will output a JSON version of the contents of each of your categories.
var toolbox = Blockly.getMainWorkspace().getToolbox();
function stripIds(blockState) {
if (!blockState) {
return;
}
delete blockState['id'];
var inputs = blockState['inputs'];
for (var name in inputs) {
stripIds(inputs[name]['block']);
stripIds(inputs[name]['shadow']);
}
if (blockState['next']) {
stripIds(blockState['next']['block']);
stripIds(blockState['next']['shadow']);
}
}
var categories = [];
var items = toolbox.getToolboxItems();
for (var i = 0; i < items.length; i++) {
// Skip separators and anything else that isn't a selectable category.
if (!items[i].isSelectable()) {
continue;
}
toolbox.selectItemByPosition(i);
toolbox.refreshSelection();
var flyout = toolbox.getFlyout();
if (!flyout) {
continue;
}
var category = [];
categories.push(category);
var blocks = flyout.getWorkspace().getTopBlocks();
for (var j = 0; j < blocks.length; j++) {
var block = blocks[j];
var state = Blockly.serialization.blocks.save(
block, {addCoordinates: false, doFullSerialization: true});
stripIds(state);
category.push(state);
}
}
console.log(JSON.stringify(categories, undefined, 2));
- Use the documentation and these resulting definitions to create the JSON definition of your toolbox.
Upgrading starter blocks
Starter blocks are blocks that you load into the workspace by default. With the old system you specified these as XML, but now you can specify them as JSON.
Here is the code we will be using as an example:
var xml = '<xml>' +
'<block type="start_block" deletable="false" movable="false" editable="false">' +
'</block>' +
'</xml>';
Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(xml), workspace);
- Get the JSON version of your blocks.
You can do this by loading the blocks into your workspace, and then runningBlockly.serialization.workspaces.save
var json = {
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "start_block",
"deletable": false,
"movable": false,
"editable": false
}
]
}
}
// Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(xml), workspace);
- Change
Blockly.Xml.domToWorkspacetoBlockly.serialization.workspaces.load.
var json = {
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "start_block",
"deletable": false,
"movable": false,
"editable": false
}
]
}
}
Blockly.serialization.workspaces.load(json, workspace);
Upgrading Event handling
For Block change events,
if the change represents a mutation, the oldValue/newValue might be
stringified JSON (rather than XML). This occurs if the block being mutated has
JSON serialization hooks. This is true of built-in blocks.
Block delete events also include oldJson and wasShadow. If your block
includes JSON hooks, or fields that use JSON hooks, you will want to examine
these properties rather than oldXml. This is true of built-in blocks.
You should check to make sure that any event listeners you’re using are set up to handle these cases.
Upgrading serialization
Now you are ready to change how your application actually saves and loads state. The basic idea is you change your function calls:
Blockly.Xml.workspaceToDom->Blockly.serialization.workspaces.saveBlockly.Xml.domToWorkspace->Blockly.serialization.workspaces.load
But if you do this, you won’t be able to load old XML saves, which breaks existing users. How you deal with this is very dependent on your storage solution. Here are a few options:
- Bulk update all of your current saves.
var xmlSave = getSave(); // However your application handles this.
var workspace = new Blockly.Workspace(); // Create a headless workspace.
Blockly.Xml.domToWorkspace(Blockly.utils.xml.textToDom(xmlSave), workspace);
var jsonSave = Blockly.serialization.workspaces.save(workspace);
saveSave(jsonSave); // However your application handles this.
- Tag new saves as JSON. Load untagged saves via the old XML system, and tagged saves via the new JSON system.
Testing
Now that everything is upgraded, you should test that all of your custom blocks and custom fields round-trip (meaning they serialize, then deserialize) properly.