Register

Version 10 Data Model Changes

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();

This migration guide provides an overview of the migration steps related to the following issues: