DataModels et TypeDataModel
Les DataModels sont le système moderne de définition et validation des données dans FoundryVTT. Apprenez à définir des schémas robustes pour vos personnages et objets.
Pourquoi DataModel vs template.json ?
Avant FoundryVTT v10, les systèmes utilisaient un fichier template.json pour définir
la structure des données. Cette approche a été remplacée par les DataModels,
qui offrent de nombreux avantages :
| Ancienne méthode (template.json) | Nouvelle méthode (DataModel) |
|---|---|
| Définition en JSON statique | Typage fort en JavaScript |
| Pas de validation | Validation automatique des données |
| Valeurs par défaut basiques | Valeurs par défaut dynamiques et intelligentes |
| Migration manuelle complexe | Migrations intégrées avec migrateData() |
| Pas d'autocomplétion IDE | Autocomplétion et documentation inline |
Pour tout nouveau système FoundryVTT v11+, utilisez exclusivement les DataModels.
Le fichier template.json est déprécié.
Hiérarchie des classes
foundry.abstract.DataModel
│
├── foundry.abstract.TypeDataModel ← Pour les documents avec sous-types
│ │
│ └── SystemDataModel (personnalisé)
│ │
│ ├── CharacterData
│ ├── NPCData
│ └── WeaponData
│
└── Autres DataModels (settings, embedded, etc.)
DataModel vs TypeDataModel
| Aspect | DataModel | TypeDataModel |
|---|---|---|
| Usage | Données génériques | Documents système (Actor, Item) |
| Accès parent | this.parent |
this.parent (le Document) |
| Sous-types | Non | Oui (character, npc, weapon...) |
- TypeDataModel : Pour les données
systemdes Actor, Item, ActiveEffect, ChatMessage - DataModel : Pour les données imbriquées, les settings, les structures personnalisées
Définir un schéma avec defineSchema()
La méthode statique defineSchema() retourne un objet décrivant la structure des données.
Syntaxe de base
// Importer les types de champs
const {
StringField, NumberField, BooleanField,
SchemaField, ArrayField, SetField,
HTMLField, FilePathField
} = foundry.data.fields;
export default class CharacterData extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
// Champ texte simple
name: new StringField({
required: true,
blank: false,
initial: "Nouveau Personnage"
}),
// Champ numérique
level: new NumberField({
required: true,
nullable: false,
integer: true,
min: 1,
max: 20,
initial: 1
}),
// Champ booléen
isNPC: new BooleanField({
initial: false
}),
// Champ HTML enrichi
biography: new HTMLField({
required: true,
blank: true
}),
// Objet imbriqué avec SchemaField
attributes: new SchemaField({
hp: new SchemaField({
value: new NumberField({ integer: true, min: 0, initial: 10 }),
max: new NumberField({ integer: true, min: 0, initial: 10 }),
temp: new NumberField({ integer: true, min: 0, initial: 0 })
}),
ac: new NumberField({ integer: true, min: 0, initial: 10 })
}),
// Tableau de valeurs
languages: new ArrayField(
new StringField({ blank: false })
),
// Ensemble (valeurs uniques)
proficiencies: new SetField(
new StringField({ blank: false })
)
};
}
}
Types de champs disponibles
Champs primitifs
| Champ | Description | Options clés |
|---|---|---|
StringField |
Texte | blank, trim, choices |
NumberField |
Nombre | integer, positive, min, max, step |
BooleanField |
Booléen | - |
HTMLField |
HTML enrichi | blank |
FilePathField |
Chemin de fichier | categories |
ColorField |
Couleur hexadécimale | - |
JSONField |
Données JSON | - |
Champs composés
| Champ | Description | Usage |
|---|---|---|
SchemaField |
Objet avec sous-schéma | Grouper des champs liés |
ArrayField |
Tableau d'éléments | Listes dynamiques |
SetField |
Ensemble (valeurs uniques) | Tags, proficiencies |
EmbeddedDataField |
DataModel imbriqué | Structures réutilisables |
MappingField |
Dictionnaire clé/valeur | Objets dynamiques |
Champs spéciaux
| Champ | Description | Usage |
|---|---|---|
DocumentIdField |
ID de document | Références internes |
DocumentUUIDField |
UUID de document | Références cross-compendium |
ForeignDocumentField |
Référence à un autre document | Relations |
Options des champs
new NumberField({
// ═══════════════════════════════════════
// OPTIONS GÉNÉRALES (tous les champs)
// ═══════════════════════════════════════
required: true, // Le champ doit exister
nullable: false, // Peut être null ?
initial: 0, // Valeur par défaut
readonly: false, // Modification interdite après création
label: "MONSYSTEME.Level", // Clé de traduction
hint: "MONSYSTEME.LevelHint", // Aide contextuelle
// Validation personnalisée
validate: (value) => value > 0,
validationError: "La valeur doit être positive",
// ═══════════════════════════════════════
// OPTIONS SPÉCIFIQUES NumberField
// ═══════════════════════════════════════
integer: true, // Nombre entier uniquement
positive: true, // Strictement positif
min: 0, // Valeur minimale
max: 100, // Valeur maximale
step: 1 // Incrément
});
new StringField({
// ═══════════════════════════════════════
// OPTIONS SPÉCIFIQUES StringField
// ═══════════════════════════════════════
blank: true, // Chaîne vide autorisée
trim: true, // Supprimer les espaces
choices: ["a", "b", "c"] // Valeurs autorisées (enum)
});
- Utilisez
nullable: falsequand possible pour éviter lesnull - Définissez toujours
initialpour avoir des valeurs par défaut cohérentes - Utilisez
labelpour l'internationalisation
Exemple complet : CharacterData
// module/data/actor/character.mjs
const {
ArrayField, BooleanField, HTMLField,
NumberField, SchemaField, SetField, StringField
} = foundry.data.fields;
export default class CharacterData extends foundry.abstract.TypeDataModel {
// Type système pour l'enregistrement
static _systemType = "character";
// Définition du schéma
static defineSchema() {
return {
// Informations de base
race: new StringField({ initial: "" }),
class: new StringField({ initial: "" }),
level: new NumberField({
integer: true,
min: 1,
max: 20,
initial: 1
}),
// Points de vie
attributes: new SchemaField({
hp: new SchemaField({
value: new NumberField({
integer: true,
min: 0,
initial: 10
}),
max: new NumberField({
integer: true,
min: 0,
initial: 10
}),
temp: new NumberField({
integer: true,
min: 0,
initial: 0
})
}),
ac: new NumberField({
integer: true,
min: 0,
initial: 10
}),
initiative: new NumberField({
integer: true,
initial: 0
})
}),
// Caractéristiques
abilities: new SchemaField({
str: this._makeAbilityField(),
dex: this._makeAbilityField(),
con: this._makeAbilityField(),
int: this._makeAbilityField(),
wis: this._makeAbilityField(),
cha: this._makeAbilityField()
}),
// Biographie (HTML enrichi)
biography: new HTMLField({ initial: "" }),
// Langues connues
languages: new ArrayField(
new StringField({ blank: false })
),
// Maîtrises
proficiencies: new SetField(
new StringField({ blank: false })
),
// Ressources
resources: new SchemaField({
primary: this._makeResourceField(),
secondary: this._makeResourceField()
})
};
}
// Helper : créer un champ de caractéristique
static _makeAbilityField() {
return new SchemaField({
value: new NumberField({
integer: true,
min: 1,
max: 30,
initial: 10
}),
proficient: new BooleanField({ initial: false })
});
}
// Helper : créer un champ de ressource
static _makeResourceField() {
return new SchemaField({
value: new NumberField({ integer: true, initial: 0 }),
max: new NumberField({ integer: true, initial: 0 }),
label: new StringField({ initial: "" })
});
}
// ═══════════════════════════════════════════════════════════
// PRÉPARATION DES DONNÉES
// ═══════════════════════════════════════════════════════════
prepareBaseData() {
// Calculer le bonus de maîtrise
this.proficiency = Math.floor((this.level + 7) / 4);
}
prepareDerivedData() {
// Calculer les modificateurs de caractéristiques
for (const [key, ability] of Object.entries(this.abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2);
ability.save = ability.mod + (ability.proficient ? this.proficiency : 0);
}
// Calculer l'initiative
this.attributes.initiative = this.abilities.dex.mod;
}
// Données pour les jets de dés
getRollData() {
return {
level: this.level,
prof: this.proficiency,
abilities: Object.fromEntries(
Object.entries(this.abilities).map(([k, v]) => [k, v.mod])
)
};
}
}
Système de templates (Mixins)
Pour éviter la duplication de code, vous pouvez créer des templates réutilisables entre différents types de DataModels.
Créer un template
// module/data/actor/templates/common.mjs
const { NumberField, SchemaField } = foundry.data.fields;
export default class CommonTemplate extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
// Caractéristiques communes à tous les acteurs
abilities: new SchemaField({
str: this._makeAbility(),
dex: this._makeAbility(),
con: this._makeAbility(),
int: this._makeAbility(),
wis: this._makeAbility(),
cha: this._makeAbility()
}),
// Monnaie
currency: new SchemaField({
pp: new NumberField({ integer: true, min: 0, initial: 0 }),
gp: new NumberField({ integer: true, min: 0, initial: 0 }),
sp: new NumberField({ integer: true, min: 0, initial: 0 }),
cp: new NumberField({ integer: true, min: 0, initial: 0 })
})
};
}
static _makeAbility() {
return new SchemaField({
value: new NumberField({ integer: true, min: 1, max: 30, initial: 10 })
});
}
}
Utiliser le mixin
// module/data/actor/character.mjs
import CommonTemplate from "./templates/common.mjs";
// Méthode 1 : Héritage simple
export default class CharacterData extends CommonTemplate {
static defineSchema() {
return this.mergeSchema(super.defineSchema(), {
// Champs spécifiques au personnage
level: new NumberField({ integer: true, min: 1, initial: 1 }),
xp: new NumberField({ integer: true, min: 0, initial: 0 })
});
}
}
// Méthode 2 : Mixin multiple (si disponible)
export default class CharacterData extends SystemDataModel.mixin(
CommonTemplate,
CreatureTemplate,
AttributesTemplate
) {
static defineSchema() {
return this.mergeSchema(super.defineSchema(), {
// Champs spécifiques
});
}
}
Migration des données
Quand votre schéma évolue, vous devez migrer les anciennes données.
La méthode migrateData() permet de transformer automatiquement les données.
Méthode migrateData()
static migrateData(source) {
// Exemple : Migrer l'ancien format de vitesse
if ("speed" in source && typeof source.speed === "number") {
source.attributes ??= {};
source.attributes.movement ??= {};
source.attributes.movement.walk = source.speed;
delete source.speed;
}
// Exemple : Renommer un champ
if ("hp" in source && !("attributes" in source)) {
source.attributes = {
hp: source.hp
};
delete source.hp;
}
// Exemple : Convertir un type de données
if (typeof source.level === "string") {
source.level = parseInt(source.level) || 1;
}
// Appeler la migration parente
return super.migrateData(source);
}
Rétrocompatibilité avec shimData()
static shimData(data, options) {
// Ajouter un getter pour l'ancien chemin (déprécié)
this._addDataFieldShim(data, "speed", "attributes.movement.walk", {
since: "2.0",
until: "3.0"
});
return super.shimData(data, options);
}
migrateData() est appelé avant la validation du schéma.
C'est l'endroit idéal pour transformer les données anciennes vers le nouveau format.
Validation des données
Validation automatique
FoundryVTT valide automatiquement les données selon votre schéma :
// Ces données seront validées automatiquement
const actor = await Actor.create({
name: "Test",
type: "character",
system: {
level: 25 // ❌ Erreur : max est 20
}
});
Validation personnalisée
static defineSchema() {
return {
hp: new SchemaField({
value: new NumberField({
validate: (value, options) => {
const max = options?.source?.max ?? Infinity;
if (value > max) {
throw new Error("HP cannot exceed max HP");
}
}
}),
max: new NumberField({ min: 0 })
})
};
}
Validation jointe avec validateJoint()
// Valider plusieurs champs ensemble
static validateJoint(data) {
super.validateJoint(data);
if (data.hp.value > data.hp.max) {
throw new foundry.data.validation.DataModelValidationError(
"Current HP cannot exceed maximum HP"
);
}
if (data.level < 1 && data.xp > 0) {
throw new foundry.data.validation.DataModelValidationError(
"Characters with XP must be at least level 1"
);
}
}
Enregistrement des DataModels
// module/data/actor/_module.mjs
import CharacterData from "./character.mjs";
import NPCData from "./npc.mjs";
import MonsterData from "./monster.mjs";
export const config = {
character: CharacterData,
npc: NPCData,
monster: MonsterData
};
// module/data/item/_module.mjs
import WeaponData from "./weapon.mjs";
import ArmorData from "./armor.mjs";
import SpellData from "./spell.mjs";
export const config = {
weapon: WeaponData,
armor: ArmorData,
spell: SpellData
};
// main.mjs - Hook init
import * as actorModels from "./module/data/actor/_module.mjs";
import * as itemModels from "./module/data/item/_module.mjs";
Hooks.once("init", function() {
// Enregistrer les DataModels pour les Actors
CONFIG.Actor.dataModels = actorModels.config;
// Enregistrer les DataModels pour les Items
CONFIG.Item.dataModels = itemModels.config;
console.log("DataModels enregistrés !");
});
Les clés du dictionnaire (character, npc, etc.)
doivent correspondre aux types déclarés dans documentTypes de votre system.json.
🎯 Exercice : Créer un DataModel pour un monstre
Créez un DataModel MonsterData avec les caractéristiques suivantes :
- cr (Challenge Rating) : nombre décimal entre 0 et 30
- size : chaîne parmi "tiny", "small", "medium", "large", "huge", "gargantuan"
- type : chaîne (ex: "dragon", "undead", "humanoid")
- hp : objet avec
value,max,formula(string) - ac : objet avec
valueettype(string) - speeds : objet avec
walk,fly,swim(nombres) - description : HTML
- legendaryActions : nombre entier min 0
Voir la solution
// module/data/actor/monster.mjs
const {
NumberField, StringField, SchemaField, HTMLField
} = foundry.data.fields;
export default class MonsterData extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
cr: new NumberField({
required: true,
nullable: false,
min: 0,
max: 30,
initial: 1
}),
size: new StringField({
required: true,
choices: ["tiny", "small", "medium", "large", "huge", "gargantuan"],
initial: "medium"
}),
type: new StringField({
required: true,
initial: "humanoid"
}),
hp: new SchemaField({
value: new NumberField({ integer: true, min: 0, initial: 10 }),
max: new NumberField({ integer: true, min: 0, initial: 10 }),
formula: new StringField({ initial: "2d8+2" })
}),
ac: new SchemaField({
value: new NumberField({ integer: true, min: 0, initial: 10 }),
type: new StringField({ initial: "natural armor" })
}),
speeds: new SchemaField({
walk: new NumberField({ integer: true, min: 0, initial: 30 }),
fly: new NumberField({ integer: true, min: 0, initial: 0 }),
swim: new NumberField({ integer: true, min: 0, initial: 0 })
}),
description: new HTMLField({ initial: "" }),
legendaryActions: new NumberField({
integer: true,
min: 0,
initial: 0
})
};
}
// Calculer le bonus de maîtrise selon le CR
prepareDerivedData() {
if (this.cr < 5) this.proficiency = 2;
else if (this.cr < 9) this.proficiency = 3;
else if (this.cr < 13) this.proficiency = 4;
else if (this.cr < 17) this.proficiency = 5;
else if (this.cr < 21) this.proficiency = 6;
else if (this.cr < 25) this.proficiency = 7;
else if (this.cr < 29) this.proficiency = 8;
else this.proficiency = 9;
}
}
Ressources
- DataModel API - Documentation officielle
- TypeDataModel API - Documentation officielle
- Data Fields - Tous les types de champs disponibles