define-entity
Use this skill when defining ROM Structs and Entities in Hanami 2.x.
Core Rules
Create the Entity file in the app or slice:
# app/entities/user.rb # frozen_string_literal: true module MyApp module Entities class User < Hanami::DB::Entity attribute :id, Types::Integer attribute :email, Types::String attribute :first_name, Types::String attribute :last_name, Types::String attribute :role, Types::String.default("member") attribute :created_at, Types::Time end end endApply dry-types coercion and constraints: Specify attribute types using standard dry-types constraints. For a comprehensive list of type modifiers, constraints, and defaults, see TYPES.md.
attribute :email, Types::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.\w+\z/) attribute :role, Types::String.default("member")Entities are immutable: Entities cannot be mutated in place. Use
#copyto return a new instance with updated attributes:updated_user = user.copy(first_name: "Alicia")Equality is value-based: Two entity instances with identical attributes are considered equivalent:
user1 == user2 # => true if attributes matchRegister the Entity namespace in Repositories: Wired repositories require mapping directives to output typed Entity classes rather than generic ROM structs:
class UserRepo < Hanami::DB::Repo[:users] struct_namespace MyApp::Entities auto_struct true endSync Entities with migrations: Keep entity class attributes manually updated with database schema modifications. Verify using the Hanami console or spec suite:
bundle exec hanami console # check MyApp::App[:user_repo].users.first.class.attributes
Common Mistakes
| Mistake | Resolution |
|---|---|
Attempting in-place mutations (user.name = "new") |
Entities are frozen. Always use user.copy(name: "new"). |
| Out of sync migrations | Stale attributes cause UnknownAttributeError. Sync entity attributes with database migrations. |
| Putting business logic inside Entities | Entities are pure data structs. Place logic in domain services or actions. |
Omitting struct_namespace configuration |
Omitting this returns generic ROM::Struct objects instead of your custom Entity class. |
Missing .optional on nullable fields |
Null database columns require Types::String.optional to prevent boot/coercion type errors. |