Castkit is a lightweight, type-safe data object system for Ruby. It provides a declarative DSL for defining data transfer objects (DTOs) with built-in support for typecasting, validation, nested data structures, serialization, deserialization, and contract-driven programming.
Inspired by tools like Jackson (Java) and Python dataclasses, Castkit brings structured data modeling to Ruby in a way that emphasizes:
- Simplicity: Minimal API surface and predictable behavior.
- Explicitness: Every field and type is declared clearly.
- Composition: Support for nested objects, collections, and modular design.
- Performance: Fast and efficient with minimal runtime overhead.
- Extensibility: Easy to extend with custom types, serializers, and integrations.
Castkit is designed to work seamlessly in service-oriented and API-driven architectures, providing structure without overreach.
- Configuration
- Attribute DSL
- DataObjects
- Contracts
- Advance Usage
- Plugins
- Castkit CLI
- Testing
- Compatibility
- License
Castkit provides a global configuration interface to customize behavior across the entire system. You can configure Castkit by passing a block to Castkit.configure.
Castkit.configure do |config|
config.enable_warnings = false
config.enforce_typing = true
end| Option | Type | Default | Description |
|---|---|---|---|
enable_warnings |
Boolean | true |
Enables runtime warnings for misconfigurations. |
enforce_typing |
Boolean | true |
Raises if type mismatch during load (e.g., true vs. "true"). |
enforce_attribute_access |
Boolean | true |
Raises if an unknown access level is defined. |
enforce_unwrapped_prefix |
Boolean | true |
Requires unwrapped: true when using attribute prefixes. |
enforce_array_options |
Boolean | true |
Raises if an array attribute is missing the of: option. |
raise_type_errors |
Boolean | true |
Raises if an unregistered or invalid type is used. |
strict_by_default |
Boolean | true |
Applies strict: true by default to all DTOs and Contracts. |
Castkit comes with built-in support for primitive types and allows registration of custom ones:
{
array: Castkit::Types::Collection,
boolean: Castkit::Types::Boolean,
date: Castkit::Types::Date,
datetime: Castkit::Types::DateTime,
float: Castkit::Types::Float,
hash: Castkit::Types::Base,
integer: Castkit::Types::Integer,
string: Castkit::Types::String
}| Alias | Canonical |
|---|---|
collection |
array |
bool |
boolean |
int |
integer |
map |
hash |
number |
float |
str |
string |
timestamp |
datetime |
uuid |
string |
Castkit.configure do |config|
config.register_type(:mytype, MyTypeClass, aliases: [:custom])
endCastkit attributes define the shape, type, and behavior of fields on a DataObject. Attributes are declared using the attribute method or shorthand type methods provided by Castkit::Core::AttributeTypes.
class UserDto < Castkit::DataObject
string :name, required: true
boolean :admin, default: false
array :tags, of: :string, ignore_nil: true
endCastkit supports a strict set of primitive types defined in Castkit::Configuration::DEFAULT_TYPES and aliased in TYPE_ALIASES.
:array:boolean:date:datetime:float:hash:integer:string
Castkit provides shorthand aliases for common primitive types:
| Alias | Canonical | Description |
|---|---|---|
collection |
array |
Alias for arrays |
bool |
boolean |
Alias for true/false types |
int |
integer |
Alias for integer values |
map |
hash |
Alias for hashes (key-value pairs) |
number |
float |
Alias for numeric values |
str |
string |
Alias for strings |
timestamp |
datetime |
Alias for date-time values |
uuid |
string |
Commonly used for identifiers |
No other types are supported unless explicitly registered via Castkit.configuration.register_type.
| Option | Type | Default | Description |
|---|---|---|---|
required |
Boolean | true |
Whether the field is required on initialization. |
default |
Object/Proc | nil |
Default value or lambda called at runtime. |
access |
Array | [:read, :write] |
Controls read/write visibility. |
ignore_nil |
Boolean | false |
Exclude nil values from serialization. |
ignore_blank |
Boolean | false |
Exclude empty strings, arrays, and hashes. |
ignore |
Boolean | false |
Fully ignore the field (no serialization/deserialization). |
composite |
Boolean | false |
Used for computed, virtual fields. |
transient |
Boolean | false |
Excluded from serialized output. |
unwrapped |
Boolean | false |
Merges nested DataObject fields into parent. |
prefix |
String | nil |
Used with unwrapped to prefix keys. |
aliases |
Array | [] |
Accept alternative keys during deserialization. |
of: |
Symbol | nil |
Required for :array attributes. |
validator: |
Proc | nil |
Optional callable that validates the value. |
Access determines when the field is considered readable/writable.
string :email, access: [:read]
string :password, access: [:write]Castkit supports grouping attributes using required and optional blocks to reduce repetition and improve clarity when defining large DTOs.
class UserDto < Castkit::DataObject
required do
string :id
string :name
end
optional do
integer :age
boolean :admin
end
endThis is equivalent to:
class UserDto < Castkit::DataObject
string :id # required: true
string :name # required: true
integer :age, required: false
boolean :admin, required: false
endGrouped declarations are especially useful when your DTO has many optional fields or a mix of required/optional fields across different types.
class Metadata < Castkit::DataObject
string :locale
end
class PageDto < Castkit::DataObject
dataobject :metadata, Metadata, unwrapped: true, prefix: "meta"
end
# Serializes as:
# { "meta_locale": "en" }Composite fields are computed virtual attributes:
class ProductDto < Castkit::DataObject
string :name, required: true
string :sku, access: [:read]
float :price, default: 0.0
composite :description, :string do
"#{name}: #{sku} - #{price}"
end
endTransient fields are excluded from serialization and can be defined in two ways:
class ProductDto < Castkit::DataObject
string :id, transient: true
transient do
string :internal_token
end
endstring :email, aliases: ["emailAddress", "user.email"]
dto.load({ "emailAddress" => "foo@bar.com" })class ProductDto < Castkit::DataObject
string :name, required: true
float :price, default: 0.0, validator: ->(v) { raise "too low" if v < 0 }
array :tags, of: :string, ignore_blank: true
string :sku, access: [:read]
composite :description, :string do
"#{name}: #{sku} - #{price}"
end
transient do
string :id
end
endCastkit::DataObject is the base class for all structured DTOs. It offers a complete lifecycle for data ingestion, transformation, and output, supporting strict typing, validation, access control, aliasing, serialization, and root-wrapped payloads.
class UserDto < Castkit::DataObject
string :id
string :name
integer :age, required: false
enduser = UserDto.new(name: "Alice", age: 30)
user.to_h #=> { name: "Alice", age: 30 }
user.to_json #=> '{"name":"Alice","age":30}'By default, Castkit operates in strict mode and raises if unknown keys are passed. You can override this:
class LooseDto < Castkit::DataObject
strict false
ignore_unknown true # equivalent to strict false
warn_on_unknown true # emits a warning instead of raising
endTo build a relaxed version dynamically:
LooseClone = MyDto.relaxed(warn_on_unknown: true)class WrappedDto < Castkit::DataObject
root :user
string :name
end
WrappedDto.new(name: "Test").to_h
#=> { "user" => { "name" => "Test" } }You can deserialize using:
UserDto.from_h(hash)
UserDto.deserialize(hash)contract = UserDto.to_contract
UserDto.validate!(id: "123", name: "Alice")
from_contract = Castkit::DataObject.from_contract(contract)To override default serialization behavior:
class CustomSerializer < Castkit::Serializers::Base
def call
{ payload: object.to_h }
end
end
class MyDto < Castkit::DataObject
string :field
serializer CustomSerializer
enddto = UserDto.new(name: "Alice", foo: "bar")
dto.unknown_attributes
#=> { foo: "bar" }UserDto.register!(as: :User)
# Registers under Castkit::DataObjects::UserCastkit::Contract provides a lightweight mechanism for validating structured input without requiring a full data model. Ideal for validating service inputs, API payloads, or command parameters.
You can define a contract using the .build DSL:
UserContract = Castkit::Contract.build(:user) do
string :id
string :email, required: false
endOr subclass directly:
class MyContract < Castkit::Contract::Base
string :id
integer :count, required: false
endUserContract.validate(id: "123")
UserContract.validate!(id: "123")Returns a Castkit::Contract::Result with:
#success?/#failure?#errorshash#to_h/#to_s
LooseContract = Castkit::Contract.build(:loose, strict: false) do
string :token
end
StrictContract = Castkit::Contract.build(:strict, allow_unknown: false, warn_on_unknown: true) do
string :id
endclass UserDto < Castkit::DataObject
string :id
string :email
end
UserContract = Castkit::Contract.from_dataobject(UserDto)UserDto = UserContract.to_dataobject
# or
UserDto = UserContract.dataobjectUserContract.register!(as: :UserInput)
# Registers under Castkit::Contracts::UserInputOnly a subset of options are supported:
requiredaliasesmin,max,formatof(for arrays)validatorunwrapped,prefixforce_type
class AddressDto < Castkit::DataObject
string :city
end
class UserDto < Castkit::DataObject
string :id
dataobject :address, AddressDto
end
UserContract = Castkit::Contract.from_dataobject(UserDto)
UserContract.validate!(id: "abc", address: { city: "Boston" })Castkit is designed to be modular and extendable. Future guides will cover:
- Custom serializers (
Castkit::Serializers::Base) - Integration layers:
castkit-activerecordfor syncing with ActiveRecord modelscastkit-msgpackfor binary encodingcastkit-ojfor high-performance JSON
- OpenAPI-compatible schema generation
- Declarative enums and union type helpers
- Circular reference detection in nested serialization
Castkit supports modular extensions through a lightweight plugin system. Plugins can modify or extend the behavior of Castkit::DataObject classes, such as adding serialization support, transformation helpers, or framework integrations.
Plugins are just Ruby modules and can be registered and activated globally or per-class.
Plugins can be activated on any DataObject or at runtime:
module MyPlugin
def self.setup!(klass)
# Optional: called after inclusion
klass.string :plugin_id
end
def plugin_feature
"Enabled!"
end
end
Castkit.configure do |config|
config.register_plugin(:my_plugin, MyPlugin)
end
class MyDto < Castkit::DataObject
enable_plugins :my_plugin
endThis includes the MyPlugin module into MyDto and calls MyPlugin.setup!(MyDto) if defined.
Plugins must be registered before use:
Castkit.configure do |config|
config.register_plugin(:oj, Castkit::Plugins::Oj)
endYou can then activate them:
Castkit::Plugins.activate(MyDto, :oj)Or by using the enable_plugins helper method in Castkit::DataObject:
class MyDto < Castkit::DataObject
enable_plugins :oj, :yaml
end| Method | Description |
|---|---|
Castkit::Plugins.register(:name, mod) |
Registers a plugin under a custom name. |
Castkit::Plugins.activate(klass, *names) |
Includes one or more plugins into a class. |
Castkit::Plugins.lookup!(:name) |
Looks up the plugin by name or constant. |
Castkit looks for plugins under the Castkit::Plugins namespace by default:
module Castkit
module Plugins
module Oj
def self.setup!(klass)
klass.include SerializationSupport
end
end
end
endTo activate this:
Castkit::Plugins.activate(MyDto, :oj)You can also manually register plugins not under this namespace.
module Castkit
module Plugins
module Timestamps
def self.setup!(klass)
klass.datetime :created_at
klass.datetime :updated_at
end
end
end
end
Castkit::Plugins.activate(UserDto, :timestamps)This approach allows reusable, modular feature sets across DTOs with clean setup behavior.
Castkit includes a command-line interface to help scaffold and inspect DTO components with ease.
The CLI is structured around two primary commands:
castkit generateβ scaffolds boilerplate for Castkit components.castkit listβ introspects and displays registered or defined components.
The castkit generate command provides subcommands for creating files for all core Castkit component types.
castkit generate dataobject User name:string age:integerCreates:
lib/castkit/data_objects/user.rbspec/castkit/data_objects/user_spec.rb
castkit generate contract UserInput id:string email:stringCreates:
lib/castkit/contracts/user_input.rbspec/castkit/contracts/user_input_spec.rb
castkit generate plugin OjCreates:
lib/castkit/plugins/oj.rbspec/castkit/plugins/oj_spec.rb
castkit generate validator MoneyCreates:
lib/castkit/validators/money.rbspec/castkit/validators/money_spec.rb
castkit generate type moneyCreates:
lib/castkit/types/money.rbspec/castkit/types/money_spec.rb
castkit generate serializer JsonCreates:
lib/castkit/serializers/json.rbspec/castkit/serializers/json_spec.rb
You can disable test generation with --no-spec.
The castkit list command provides an interface to view internal Castkit definitions or project-registered components.
castkit list typesDisplays a grouped list of:
- Native types (defined by Castkit)
- Custom types (registered via
Castkit.configure)
Example:
Native Types:
Castkit::Types::String - :string, :str, :uuid
Custom Types:
MyApp::Types::Money - :moneycastkit list validatorsDisplays all validator classes defined in lib/castkit/validators or custom-defined under Castkit::Validators.
Castkit validators are tagged [Castkit], and others as [Custom].
castkit list contractsLists all contracts in the Castkit::Contracts namespace and related files.
castkit list dataobjectsLists all DTOs in the Castkit::DataObjects namespace.
castkit list serializersLists all serializer classes and their source origin.
castkit generate dataobject Product name:string price:float
castkit generate contract ProductInput name:string
castkit list types
castkit list validatorsThe CLI is designed to provide a familiar Rails-like generator experience, tailored for Castkitβs data-first architecture.
You can test DTOs and Contracts by treating them like plain Ruby objects:
dto = MyDto.new(name: "Alice")
expect(dto.name).to eq("Alice")You can also assert validation errors:
expect {
MyDto.new(name: nil)
}.to raise_error(Castkit::AttributeError, /name is required/)- Ruby 2.7+
- Zero dependencies (uses core Ruby)
MIT. See LICENSE.
Created with β€οΈ by Nathan Lucas