This article describes breaking changes to the Foundry Virtual Tabletop API which arrived in Version 10 and require some action by community developers to adjust their code in response.
The primary architectural innovation added in Foundry Virtual Tabletop version 10 is the new DataModel
architecture which improves upon and replaces the prior concept of DocumentData
which is now deprecated. In addition to overhauling the functionality of DocumentData
, the Document
class now directly extends DataModel
instead of including a document data instance via composition.
Deprecation Period: Until Version 13
The combination of these two changes are the most significant breaking change in V10, however they do not generally require immediate action with a backwards-compatible deprecation period supported through Version 12.
The Data Model
The new Version 10 DataModel provides as a framework to define a structured data schema from which document objects can be constructed, validated, updated, and serialized.
Most data structures and concepts in the Foundry Virtual Tabletop ecosystem can (and possibly should) be expressed as a DataModel. The most immediate use cases for such models are documents, but we have also adopted the data model for other cases in V10 ranging from ApplicationConfiguration to VisionMode definitions.
Data Schema
The foundation of any Data Model is its data schema which defines the set of fields which can belong to the model. As a major enhancement on prior versions, we now have a rich and expressive framework for defining DataField instances with many helpful field types provided.
The static schema
for every DataModel
class is now defined using these fields, as follows:
class MyDataModel extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
requiredString: new fields.StringField({required: true, blank: false}),
positiveInteger: new fields.NumberField({required: true, nullable: false, integer: true, positive: true}),
stringArray: new fields.ArrayField(new fields.StringField()),
innerSchema: new fields.SchemaField({
innerBoolean: new fields.BooleanField({initial: false}),
numberSet: new fields.SetField(new fields.NumberField({nullable: false, min: 0, max: 1}))
})
}
}
}
Data schema can be very expressive, including nested objects and structures. We encourage developers who wish to define a DataModel
for their own use cases to survey the many examples provided by the core document definitions.
Document Construction
Each constructed instance of a DataModel
is an individual object of data which is structured and validated according to its schema. Every field of the model is available as a property on the constructed data instance. For example, we can construct our defined model as:
const myData = new MyDataModel({
requiredString: "Hello",
positiveInteger: 7,
stringArray: ["one", "two", "seven"]
});
console.log(myData.positiveInteger); // 7
Attempting to construct a data model using invalid data will throw a ModelValidationError.
const myData = new MyDataModel({
requiredString: null,
positiveInteger: -1
});
// Uncaught Error: MyDataModel Model Validation Errors
// [MyDataModel.requiredString]: may not be a blank string
// [MyDataModel.positiveInteger]: may not be null
If you wish to construct a DataModel
instance using potentially unclean data, you may do so by providing {strict: false}
as a context option to the constructor. This will warn about validation failures instead of strictly raising an error.
const myData = new MyDataModel({
requiredString: null,
positiveInteger: -1
}, {strict: false});
console.log(myData.requiredString); // ""
console.log(myData.positiveInteger); // 1
Validation and Updating
A DataModel
instance is validated strictly whenever its source data is updated using the DataModel#updateSource
method.
myData.updateSource({requiredString: ""});
// Uncaught Error: MyDataModel Model Validation Errors
// [MyDataModel.requiredString]: may not be a blank string
You may test a candidate set of changes by calling the DataModel#validate
method.
Migration Guide
As a key feature of this new DataModel
architecture, the pre-existing Document class now directly extends the DataModel
and eliminates use of an internal DocumentData
concept which is now deprecated. There are several significant consequences of this change.
DocumentData Deprecation
There no longer exists an inner Document#data
object. Any code which references this data object will now generate console logged warnings and have their request redirected to the root level of the Document.
Version 9 (Before)
const actor = game.actors.get(actorId);
const schema = actor.data.constructor.schema;
const name = actor.data.name;
const trueName = actor.data._source.name;
actor.data.update({name: "New Name"});
actor.data.validate({name: "Is this valid?"});
actor.data.reset();
Version 10 (After)
const actor = game.actors.get(actorId);
const schema = actor.constructor.schema;
const name = actor.name;
const trueName = actor._source.name;
actor.updateSource({name: "New Name"});
actor.validate({name: "Is this valid?"});
actor.reset();
Renaming of System Data
While removing the inner data
object from documents, we have also made a significant change to the semantics for the inner object of system-specific data. This inner-object was previously also named "data" which created an un-informative reference pattern for field paths like actor.data.data.attributes
. The inner system-specific data object is now named system
with backwards compatibility provided for pre-existing code which continues to reference data
.
Version 9 (Before)
const actor = game.actors.get(actorId);
const systemData = actor.data.data;
const strength = actor.data.data.attributes.strength;
Version 10 (After)
const actor = game.actors.get(actorId);
const systemData = actor.system;
const strength = actor.system.attributes.strength;
When creating or updating a document, references to the inner system data object will be redirected but will produce logged warnings.
Version 9 (Before)
const newActor = await Actor.create({name: "My New Actor", type: "npc", "data.attributes.strength": 10});
newActor.update({"data.attributes.strength": 5});
Version 10 (After)
const newActor = await Actor.create({name: "My New Actor", type: "npc", "system.attributes.strength": 10});
newActor.update({"system.attributes.strength": 5});
Document Schema Changes
Required Schema Changes
As a required consequence of the above changes the inner system "data" object has been renamed to "system". Additionally, the object which tracks user-level ownership over a document has been renamed from "permission" to "ownership" to remove a collision with the Document#permission
getter which still provides the ownership level for the current user. For similar reasons, Actor#token
has been renamed to Actor#prototypeToken
to disambiguate and avoid collision with the token
getter which provides the placed Token object for un-linked Actors.
The following Document schema changes have been made as a required consequence of the above changes:
Version 9 (Before)
// System Data Rename
Actor#data#data
Card#data#data
Cards#data#data
Item#data#data
// Permission to Ownership Rename
Actor#data#permission
Cards#data#permission
JournalEntry#data#permission
Macro#data#permission
Playlist#data#permission
RollTable#data#permission
Item#data#permission
// Other Schema Changes
Actor#data#token
TableResult#data#collection
TableResult#data#resultId
Version 10 (After)
// System Data Rename
Actor#system
Card#system
Cards#system
Item#system
// Permission to Ownership Rename
Actor#ownership
Cards#ownership
JournalEntry#ownership
Macro#ownership
Playlist#ownership
RollTable#ownership
Item#ownership
// Other Schema Changes
Actor#prototypeToken
TableResult#documentCollection
TableResult#documentId
Texture Data
We implemented an inner TextureData field type to standardize the way that data related to textures rendered on the game canvas are stored and validated. We have deployed this texture data object in the following locations which change the document schema:
Version 9 (Before)
Note#data#icon
Note#data#tint
Scene#data#img
Scene#data#shiftX
Scene#data#shiftY
Tile#data#img
Tile#data#tint
Token#data#img
Token#data#tint
Token#data#mirrorX
Token#data#mirrorY
Version 10 (After)
Note#texture#src
Note#texture#tint
Scene#background#src
Scene#background#offsetX
Scene#background#offsetY
Tile#texture#src
Tile#texture#tint
Token#texture#src
Token#texture#tint
Token#texture#scaleX
Token#texture#scaleY
Drawing Shape Data
We implemented an inner ShapeData field type to standardize the way that data related to textures rendered on the game canvas are stored and validated. We have deployed this texture data object in the following locations which change the document schema:
Version 9 (Before)
Drawing#data#type
Drawing#data#width
Drawing#data#height
Drawing#data#points
Version 10 (After)
Drawing#shape#type
Drawing#shape#width
Drawing#shape#height
Drawing#shape#points
Grid Configuration Data
We implemented an inner SchemaField to store and structure data pertaining to Scene grid configuration as an inner-object of the Scene document. The following schema fields of the Scene document have been migrated.
Version 9 (Before)
Scene#data#gridType
Scene#data#gridColor
Scene#data#gridAlpha
Scene#data#gridDistance
Scene#data#gridUnits
Version 10 (After)
Scene#grid#type
Scene#grid#color
Scene#grid#alpha
Scene#grid#distance
Scene#grid#units
Document Method Changes
For reasons similar to the data schema changes mentioned above, certain methods or attributes of Document instances were forced to change in order to avoid a collision with a DataModel attribute or schema field. The following methods or attributes have been renamed:
Version 9 (Before)
// Attribute Name Changes
Card#face
PlaylistSound#volume
// Method Name Changes
Cards#reset
RollTable#reset
Version 10 (After)
// Attribute Name Changes
Card#currentFace
PlaylistSound#effectiveVolume
// Method Name Changes
Cards#recall
RollTable#resetResults
Invalid Documents
While the Version 10 software makes a careful attempt to migrate prior data to the new model, there are cases where some piece of pre-existing data may no longer be compatible with the new V10 data schema for a certain document class. In such cases that Document which cannot be constructed is placed into a sub-collection of invalid documents which allow for problems to be corrected. The server-side software will honor changes to an invalid document which transform it into a now-valid state.
// Retrieve the data for an invalid document
const invalidActorIds = Array.from(game.actors._invalidActorIds);
const invalidId = game.actors._invalidActorIds.first();
const invalidActor = game.actors.getInvalid(invalidId);
// Correct an invalid document
await invalidActor.update(correctedData);
// Delete an invalid document
await invalidActor.delete();
Related Issues
This migration guide provides an overview of the migration steps related to the following issues: