Register

Introduction to System Data Models

The DataModel class provided as foundry.abstract.DataModel is a powerful tool for defining data schema and functionality that is used throughout Foundry Virtual Tabletop. For an overview of this concept please read the introductory article about the Version 10 Data Model which was written when this architecture was first added to Foundry Virtual Tabletop.

Data Models are not solely for use by core Document types, but also empower system developers to define such Data Models in order to represent data specific to their systems. Data Models defined in this way receive many of the same benefits as their core API counterparts, including validation on initialization and update, and on-the-fly data migration and coercion. It also allows functionality to be delegated out to these models, reducing the burden on the system's Document implementation to solely house such logic.

This article focuses on how to utilize Data Models for a Game System in Foundry Virtual Tabletop. For more detailed information on Data Models themselves please read the above article or refer to the classes/foundry.abstract.DataModel API Documentation.

Defining a Data Model

Data Models themselves are lightweight classes that can be defined very simply. Here we will take an example system template and construct a corresponding Data Model for it.

Data Model

class CharacterData extends foundry.abstract.DataModel {
  static defineSchema() {
    const fields = foundry.data.fields;
    return {
      biography: new fields.HTMLField(),
      health: new fields.SchemaField({
        value: new fields.NumberField({
          required: true,
          initial: 10,
          integer: true
        }),
        min: new fields.NumberField({
          required: true,
          initial: 0,
          integer: true
        }),
        max: new fields.NumberField({
          required: true,
          initial: 10,
          integer: true
        })
      }),
      proficiencies: new fields.SchemaField({
        weapons: new fields.ArrayField(new fields.StringField()),
        skills: new fields.ArrayField(new fields.StringField())
      })
    };
  }
}

If developing for Version 10, you may wish to include the static _enableV10Validation = true; property on your DataModel class which enables more strict validation of your defined model fields. If developing for Version 11 or later this property is unnecessary.

With the full schema of your DataModel defined, it becomes unnecessary to define the schema using the template.json file. The only fields necessary in that file are to confirm the document types that your system uses, and which fields require additional server-side sanitization as htmlFields, for example:

template.json

{
  "Actor": {
    "types": ["character"],
    "htmlFields": ["biography"],
    "character": {}
  }
}

Registering a Data Model

Once the Data Model is defined, the core API can be made aware of it and will automatically apply the data model to the system field of any registered types.

The following code snippet will register the example CharacterData model to be automatically applied as the system data for every Actor of the character type.

Hooks.on("init", () => {
  CONFIG.Actor.systemDataModels.character = CharacterData;
});

Migrations

Migrations and data coercion can be performed before your Data Model is instantiated, allowing legacy data to be converted into a format that the current version of your system understands. Migrations are run when the source data is first retrieved from disk, as well as run on any update deltas before they are applied to the Document.

class CharacterData extends foundry.abstract.DataModel {
  static defineSchema() {
    // Omitted for brevity.
  }

  /**
   * Migrate source data from some prior format into a new specification.
   * The source parameter is either original data retrieved from disk or provided by an update operation.
   * @inheritDoc
   */
  static migrateData(source) {
    if ( "weapons" in source ) {
      source.weapons = source.weapons.map(weapon => {
        return weapon === "bmr" ? "boomerang" : weapon;
      });
    }
    return super.migrateData(source);
  }
}

Enhancing a Data Model

Data Models can have methods added to them that encapsulate logic relevant to the particular system-specific type of Document that they represent. This allows you to move logic out of the Document implementation and house it in a location that is much more specific to its functionality. The parent Document instance is accessible from within the Data Model's instance methods via this.parent, allowing for more complex interactions and logic.

class CharacterData extends foundry.abstract.DataModel {
  static defineSchema() {
    // Omitted for brevity.
  }

  static migrateData() {
    // Omitted for brevity.
  }

  /**
   * Determine whether the character is dead.
   * @type {boolean}
   */
  get dead() {
    const invulnerable = CONFIG.specialStatusEffects.INVULNERABLE;
    if ( this.parent.effects.some(e => e.statuses.has("invulnerable") ) return false;
    return this.health.value <= this.health.min;
  }
}

The defined dead property could then be accessed on any Actor document of the character type as follows:

// Determine if a character is dead.
game.actors.getName("Character").system.dead;

Token Resources

When using the System template.json, the properties that can be used for a Token's resource bar are inferred from the template. This works well enough, but it can also include things like derived properties, properties that were intended to be hidden, or otherwise properties that are not suitable or ever useful as a resource bar, making it difficult for a user to locate the actual properties they want.

When using a Data Model for your system data, the core software will no longer attempt to infer which properties can be used as Token resource bars. Instead, you are given full control to tailor this list to whatever makes sense for your System. To do so, you need to modify the CONFIG.Actor.trackableAttributes configuration variable. The below example shows how to configure one resource as a bar attribute, and another as a value attribute.

Hooks.on("init", () => {
  CONFIG.Actor.trackableAttributes = {
    character: {
      bar: ["attributes.hp"],
      value: ["attributes.ac.value"]
    }
  };
});

For bar attributes, the property supplied must point to some object with both value and max properties, and these properties must both be numbers. For value attributes, the property supplied must simply point to any number. The attributes do not need to exist in your Data Model, they can be properties that are later derived as part of data preparation. If the attribute does not exist in the Data Model or is not a NumericField, then it will not be editable in the Token HUD.

In line with this feature, the signature of TokenDocument.getTrackedAttributes has changed. It will now accept a string value of the type of Actor that the resources should be retrieved for. If the argument is not supplied, and tracked attributes are configured via trackableAttributes, then getTrackedAttributes will return the union of all resources across all configured Actor types. This is useful in cases where the Actor type is not known, such as default token settings.

If your System makes use of Data Models, but you would rather have the core software infer the tracked attributes from your schema, you may opt to not configure any trackableAttributes. Bear in mind that the core API does not know the semantics of any custom DataFields you may be making use of in your Data Model, and so will not be able to inspect it for potential trackable attributes. Additionally, it will be unable to include any properties derived during data preparation, as they will not have corresponding fields in your schema. If you want to tailor the list of trackable attributes in those cases, you must override TokenDocument.getTrackedAttributes yourself. In the majority of cases, we expect that using the trackableAttributes configuration should be a lot simpler.