Model API Reference
This reference covers the complete Model API in Dojo, including storage operations, field access patterns, and performance optimizations.
Core Model Trait
Every model in Dojo implements the Model<M>
trait, which provides comprehensive functionality for model operations.
The following examples are based on the following simple Position
model:
#[derive(Drop, Serde)]
#[dojo::model]
struct Position {
#[key]
id: u32,
x: u32,
y: u32
}
Model Identity
use dojo::model::Model;
// Get the entity ID for this model instance
let entity_id: felt252 = model.entity_id();
// Get model name
let name: ByteArray = Model::<Position>::name();
// Get model selector for a namespace
let selector: felt252 = Model::<Position>::selector(namespace_hash);
// Get model definition
let definition: ModelDef = Model::<Position>::definition();
// Get model schema information
let schema: Struct = Model::<Position>::schema();
// Get model layout information
let layout: Layout = Model::<Position>::layout();
// Get specific field layout
let field_layout: Option<Layout> = Model::<Position>::field_layout(selector!("x"));
// Get model size information
let unpacked_size: Option<usize> = Model::<Position>::unpacked_size();
let packed_size: Option<usize> = Model::<Position>::packed_size();
// Get instance layout from model
let instance_layout: Layout = model.instance_layout();
Model Serialization
use dojo::model::Model;
// Serialize keys and values
let keys: Span<felt252> = model.serialized_keys();
let values: Span<felt252> = model.serialized_values();
// Reconstruct model from serialized data
let model: Option<Position> = Model::<Position>::from_serialized(keys, values);
Model Pointers
use dojo::model::{Model, ModelPtr};
// Create pointer from keys (advanced usage)
let ptr: ModelPtr<Position> = Model::<Position>::ptr_from_keys(player_address);
// Create pointers from multiple keys (advanced usage)
let ptrs: Span<ModelPtr<Position>> = Model::<Position>::ptrs_from_keys(
array![player1, player2, player3].span()
);
// Create pointer from entity ID (advanced usage)
let ptr: ModelPtr<Position> = Model::<Position>::ptr_from_id(entity_id);
// Get pointer from model instance (advanced usage)
let ptr: ModelPtr<Position> = model.ptr();
Model Storage Operations
The ModelStorage
trait provides all storage operations for models:
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage, WorldStorageTrait};
use starknet::get_caller_address;
let mut world: WorldStorage = self.world(@"my_game");
Basic Storage Operations
Writing Models
// Write single model
let player = get_caller_address();
let position = Position { player, x: 10, y: 20 };
world.write_model(@position);
// Write multiple models
let positions = array![
@Position { player: player1, x: 10, y: 20 },
@Position { player: player2, x: 30, y: 40 }
];
world.write_models(positions.span());
Reading Models
// Read single model
let player = get_caller_address();
let position: Position = world.read_model(player);
// Read multiple models
let players = array![player1, player2, player3];
let positions: Array<Position> = world.read_models(players.span());
Erasing Models
// Erase single model
world.erase_model(@position);
// Erase multiple models
world.erase_models(positions.span());
// Erase by model pointer
let ptr = Model::<Position>::ptr_from_keys(player_address);
world.erase_model_ptr(ptr);
// Erase multiple models by pointers
let ptrs = Model::<Position>::ptrs_from_keys(players.span());
world.erase_models_ptrs(ptrs);
Field-Level Operations
Field operations are more efficient when you only need to update specific fields:
Reading Fields
// Read specific field (advanced usage)
let player = get_caller_address();
let x_coord: u32 = world.read_member(
Model::<Position>::ptr_from_keys(player),
selector!("x")
);
// Read same field from multiple models (advanced usage)
let ptrs = Model::<Position>::ptrs_from_keys(players.span());
let x_coords: Array<u32> = world.read_member_of_models(ptrs, selector!("x"));
Writing Fields
// Write specific field (advanced usage)
let player = get_caller_address();
world.write_member(
Model::<Position>::ptr_from_keys(player),
selector!("x"),
42_u32
);
// Write same field for multiple models (advanced usage)
let ptrs = Model::<Position>::ptrs_from_keys(players.span());
let new_x_values = array![10, 20, 30];
world.write_member_of_models(ptrs, selector!("x"), new_x_values.span());
Schema Reading
You can read models using custom schemas that implement the Introspect
trait:
#[derive(Drop, Serde, Introspect)]
struct PositionSummary {
x: u32,
y: u32,
}
// Read using custom schema
let player = get_caller_address();
let ptr = Model::<Position>::ptr_from_keys(player);
let summary: PositionSummary = world.read_schema(ptr);
// Read multiple with custom schema
let ptrs = Model::<Position>::ptrs_from_keys(players.span());
let summaries: Array<PositionSummary> = world.read_schemas(ptrs);
Model Values
Model values contain only the non-key fields of a model:
#[derive(Drop, Serde)]
#[dojo::model]
struct Player {
#[key]
player_id: ContractAddress,
name: ByteArray,
score: u32,
level: u8,
}
// Dojo automatically generates PlayerValue:
// struct PlayerValue {
// name: ByteArray,
// score: u32,
// level: u8,
// }
Model Value Operations
The ModelValue<V>
trait provides operations for value-only access:
use dojo::model::{ModelValue};
// Get value serialization
let values: Span<felt252> = player_value.serialized_values();
// Reconstruct from serialized values
let player_value: Option<PlayerValue> = ModelValue::<PlayerValue>::from_serialized(values);
// Get value name and layout
let name: ByteArray = ModelValue::<PlayerValue>::name();
let layout: Layout = ModelValue::<PlayerValue>::layout();
// Get value selector, i.e. poseidon(namespace_hash, model_name)
let selector: felt252 = ModelValue::<PlayerValue>::selector(namespace_hash);
// Get instance layout from value
let instance_layout: Layout = player_value.instance_layout();
Model Value Storage Operations
The ModelValueStorage
trait provides value-only storage operations that work with just the non-key fields:
use dojo::model::{ModelValueStorage};
// Read value from entity ID
let player = get_caller_address();
let player_felt: felt252 = player.into();
let entity_id = dojo::utils::entity_id_from_serialized_keys([player_felt].span());
let player_value: PlayerValue = world.read_value_from_id(entity_id);
// Write value to entity ID
let new_value = PlayerValue { name: "Alice", score: 100, level: 5 };
world.write_value_from_id(entity_id, @new_value);
// Read multiple values from entity IDs
let entity_ids = array![id1, id2, id3];
let values: Array<PlayerValue> = world.read_values_from_ids(entity_ids.span());
// Write multiple values to entity IDs
let values = array![@value1, @value2, @value3];
world.write_values_from_ids(entity_ids.span(), values.span());
// Erase value from entity ID
world.erase_value_from_id(entity_id);
// Erase multiple values from entity IDs
world.erase_values_from_ids(entity_ids.span());
Performance Optimizations
Batch Operations
Use batch operations when working with multiple models:
// Efficient: batch read
let players = array![player1, player2, player3];
let positions: Array<Position> = world.read_models(players.span());
// Inefficient: individual reads
let pos1: Position = world.read_model(player1);
let pos2: Position = world.read_model(player2);
let pos3: Position = world.read_model(player3);
Field Access Patterns
Use field operations for partial updates:
// Efficient: update only needed field (advanced usage)
let player = get_caller_address();
world.write_member(
Model::<Position>::ptr_from_keys(player),
selector!("x"),
new_x_value
);
// Ergonomic: read full model, update, write back
let mut position: Position = world.read_model(player);
position.x = new_x_value;
world.write_model(@position);
Storage Layout Considerations
Choose the right introspection trait based on your needs:
// For upgradeable models (flexible but less efficient)
#[derive(Drop, Serde, Introspect)]
#[dojo::model]
struct FlexibleModel {
#[key]
id: u32,
data: ByteArray,
}
// For stable models (efficient but not upgradeable)
#[derive(Drop, Serde, IntrospectPacked)]
#[dojo::model]
struct StableModel {
#[key]
id: u32,
x: u32,
y: u32,
}
Common Patterns
Singleton Models
For global settings or counters:
const GAME_CONFIG_ID: u32 = 999999;
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct GameConfig {
#[key]
id: u32,
max_players: u32,
game_duration: u64,
}
// Usage
let config: GameConfig = world.read_model(GAME_CONFIG_ID);
Composite Key Models
For relationships between entities:
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Ownership {
#[key]
owner: ContractAddress,
#[key]
item_id: u32,
quantity: u32,
}
// Usage
let ownership: Ownership = world.read_model((owner_address, item_id));
Entity Component Pattern
Following ECS principles:
// Components
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Position {
#[key]
entity_id: u32,
x: u32,
y: u32,
}
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Health {
#[key]
entity_id: u32,
current: u32,
max: u32,
}
// Systems operate on entities with specific components
fn heal_entity(ref world: WorldStorage, entity_id: u32, amount: u32) {
let mut health: Health = world.read_model(entity_id);
health.current = core::cmp::min(health.current + amount, health.max);
world.write_model(@health);
}
Nonexistent Models
When reading non-existent models, the API returns default values (all fields set to 0) rather than failing:
// Reading a non-existent model returns default values
let player: ContractAddress = 0.try_into().unwrap();
let position: Position = world.read_model(player);
// If model doesn't exist: position.vec.x = 0, position.vec.y = 0
// For optional model reading, check if all fields are default
let position: Position = world.read_model(player);
let is_default = position.vec.x == 0 && position.vec.y == 0;
if is_default {
// Handle case where model might not exist
}
Best Practices
- Use appropriate traits: Choose between
Introspect
andIntrospectPacked
based on your needs - Batch operations: Use bulk read/write methods when working with multiple models
- Field access: Use field operations for partial updates
- Model design: Keep models small and focused (ECS principle)
- Key design: Use meaningful, collision-resistant keys
- Performance: Consider storage layout and access patterns
For more advanced usage and examples, see the Model Examples in the Dojo repository.
Complete Real-World Example
Here's a complete example from a typical Dojo game showing common patterns:
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage, WorldStorageTrait};
use starknet::{ContractAddress, get_caller_address};
#[derive(Copy, Drop, Serde, Debug)]
#[dojo::model]
struct Player {
#[key]
pub owner: ContractAddress,
pub experience: u32,
pub health: u32,
pub coins: u32,
}
#[derive(Copy, Drop, Serde, Debug)]
#[dojo::model]
struct Position {
#[key]
pub player: ContractAddress,
pub x: u32,
pub y: u32,
}
#[dojo::contract]
mod game_actions {
use super::{Player, Position};
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage};
use starknet::get_caller_address;
#[abi(embed_v0)]
impl ActionsImpl of IActions<ContractState> {
fn spawn_player(ref self: ContractState) {
let mut world = self.world(@"my_game");
let playerAddress = get_caller_address();
// Create new player
let player = Player { owner: player, experience: 0, health: 100, coins: 0 };
// Set starting position
let position = Position { player, x: 0, y: 0 };
// Write both models
world.write_model(@player);
world.write_model(@position);
}
fn move_player(ref self: ContractState, x: u32, y: u32) {
let mut world = self.world(@"my_game");
let playerAddress = get_caller_address();
// Read current position
let mut position: Position = world.read_model(playerAddress);
// Update position
position.x = x;
position.y = y;
// Write updated position
world.write_model(@position);
}
fn add_experience(ref self: ContractState, amount: u32) {
let mut world = self.world(@"my_game");
let playerAddress = get_caller_address();
// Read current player state
let mut player: Player = world.read_model(playerAddress);
// Update experience
player.experience += amount;
// Write updated player
world.write_model(@player);
}
}
}