Castkit
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.
π Features
- Configuration
- Attribute DSL
- DataObjects
- Contracts
- Advance Usage
- Plugins
- Castkit CLI
- Testing
- Compatibility
- License
Configuration
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
βοΈ Available Settings
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. |
π§ Type System
Castkit comes with built-in support for primitive types and allows registration of custom ones:
Default types
{
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
}
Type Aliases
Alias | Canonical |
---|---|
collection |
array |
bool |
boolean |
int |
integer |
map |
hash |
number |
float |
str |
string |
timestamp |
datetime |
uuid |
string |
Registering Custom Types
Castkit.configure do |config|
config.register_type(:mytype, MyTypeClass, aliases: [:custom])
end
Attribute DSL
Castkit 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
end
π§ Supported Types
Castkit supports a strict set of primitive types defined in Castkit::Configuration::DEFAULT_TYPES
and aliased in TYPE_ALIASES
.
Canonical Types:
:array
:boolean
:date
:datetime
:float
:hash
:integer
:string
Type Aliases:
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
.
βοΈ Attribute Options
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 Control
Access determines when the field is considered readable/writable.
string :email, access: [:read]
string :password, access: [:write]
π§© Attribute Grouping
Castkit supports grouping attributes using required
and optional
blocks to reduce repetition and improve clarity when defining large DTOs.
Example
class UserDto < Castkit::DataObject
required do
string :id
string :name
end
optional do
integer :age
boolean :admin
end
end
This is equivalent to:
class UserDto < Castkit::DataObject
string :id # required: true
string :name # required: true
integer :age, required: false
boolean :admin, required: false
end
Grouped declarations are especially useful when your DTO has many optional fields or a mix of required/optional fields across different types.
𧬠Unwrapped & Composite
class Metadata < Castkit::DataObject
string :locale
end
class PageDto < Castkit::DataObject
dataobject :metadata, unwrapped: true, prefix: "meta"
end
# Serializes as:
# { "meta_locale": "en" }
Composite Attributes
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
end
π Transient Attributes
Transient 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
end
πͺ Aliases and Key Paths
string :email, aliases: ["emailAddress", "user.email"]
dto.load({ "emailAddress" => "[email protected]" })
π§ͺ Example
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
end
DataObjects
Castkit::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.
βοΈ Defining a DTO
class UserDto < Castkit::DataObject
string :id
string :name
integer :age, required: false
end
π Instantiation & Usage
user = UserDto.new(name: "Alice", age: 30)
user.to_h #=> { name: "Alice", age: 30 }
user.to_json #=> '{"name":"Alice","age":30}'
βοΈ Strict Mode vs. Unknown Key Handling
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
end
To build a relaxed version dynamically:
LooseClone = MyDto.relaxed(warn_on_unknown: true)
π§± Root Wrapping
class WrappedDto < Castkit::DataObject
root :user
string :name
end
WrappedDto.new(name: "Test").to_h
#=> { "user" => { "name" => "Test" } }
π¦ Deserialization Helpers
You can deserialize using:
UserDto.from_h(hash)
UserDto.deserialize(hash)
π Conversion from/to Contract
contract = UserDto.to_contract
UserDto.validate!(id: "123", name: "Alice")
from_contract = Castkit::DataObject.from_contract(contract)
π Serializer Override
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
end
π Tracking Unknown Fields
dto = UserDto.new(name: "Alice", foo: "bar")
dto.unknown_attributes
#=> { foo: "bar" }
π€ Registering a Contract
UserDto.register!(as: :User)
# Registers under Castkit::DataObjects::User
Contracts
Castkit::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.
π Defining Contracts
You can define a contract using the .build
DSL:
UserContract = Castkit::Contract.build(:user) do
string :id
string :email, required: false
end
Or subclass directly:
class MyContract < Castkit::Contract::Base
string :id
integer :count, required: false
end
π§ͺ Validation
UserContract.validate(id: "123")
UserContract.validate!(id: "123")
Returns a Castkit::Contract::Result
with:
#success?
/#failure?
#errors
hash#to_h
/#to_s
βοΈ Strict, Loose, and Warn Modes
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
end
π Converting From DataObject
class UserDto < Castkit::DataObject
string :id
string :email
end
UserContract = Castkit::Contract.from_dataobject(UserDto)
βοΈ Converting Back to DTO
UserDto = UserContract.to_dataobject
# or
UserDto = UserContract.dataobject
π€ Registering a Contract
UserContract.register!(as: :UserInput)
# Registers under Castkit::Contracts::UserInput
π§± Supported Options in Contract Attributes
Only a subset of options are supported:
required
aliases
min
,max
,format
of
(for arrays)validator
unwrapped
,prefix
force_type
π§© Validating Nested DTOs
class AddressDto < Castkit::DataObject
string :city
end
class UserDto < Castkit::DataObject
string :id
dataobject :address, of: AddressDto
end
UserContract = Castkit::Contract.from_dataobject(UserDto)
UserContract.validate!(id: "abc", address: { city: "Boston" })
Advanced Usage (coming soon)
Castkit is designed to be modular and extendable. Future guides will cover:
- Custom serializers (
Castkit::Serializers::Base
) - Integration layers:
castkit-activerecord
for syncing with ActiveRecord modelscastkit-msgpack
for binary encodingcastkit-oj
for high-performance JSON
- OpenAPI-compatible schema generation
- Declarative enums and union type helpers
- Circular reference detection in nested serialization
Plugins
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.
π¦ Activating Plugins
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
Castkit::Plugins.activate(self, :my_plugin)
end
This includes the MyPlugin
module into MyDto
and calls MyPlugin.setup!(MyDto)
if defined.
π§© Registering Plugins
Plugins must be registered before use:
Castkit.configure do |config|
config.register_plugin(:oj, Castkit::Plugins::Oj)
end
You can then activate them:
Castkit::Plugins.activate(MyDto, :oj)
π§° Plugin API
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. |
π Plugin Structure
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
end
To activate this:
Castkit::Plugins.activate(MyDto, :oj)
You can also manually register plugins not under this namespace.
β Example Use Case
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 CLI
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.
β¨ Generate Commands
The castkit generate
command provides subcommands for creating files for all core Castkit component types.
π§± DataObject
castkit generate dataobject User name:string age:integer
Creates:
lib/castkit/data_objects/user.rb
spec/castkit/data_objects/user_spec.rb
π Contract
castkit generate contract UserInput id:string email:string
Creates:
lib/castkit/contracts/user_input.rb
spec/castkit/contracts/user_input_spec.rb
π Plugin
castkit generate plugin Oj
Creates:
lib/castkit/plugins/oj.rb
spec/castkit/plugins/oj_spec.rb
π§ͺ Validator
castkit generate validator Money
Creates:
lib/castkit/validators/money.rb
spec/castkit/validators/money_spec.rb
𧬠Type
castkit generate type money
Creates:
lib/castkit/types/money.rb
spec/castkit/types/money_spec.rb
π¦ Serializer
castkit generate serializer Json
Creates:
lib/castkit/serializers/json.rb
spec/castkit/serializers/json_spec.rb
You can disable test generation with --no-spec
.
π List Commands
The castkit list
command provides an interface to view internal Castkit definitions or project-registered components.
π§Ύ List Types
castkit list types
Displays 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 - :money
π List Validators
castkit list validators
Displays all validator classes defined in lib/castkit/validators
or custom-defined under Castkit::Validators
.
Castkit validators are tagged [Castkit]
, and others as [Custom]
.
π List Contracts
castkit list contracts
Lists all contracts in the Castkit::Contracts
namespace and related files.
π¦ List DataObjects
castkit list dataobjects
Lists all DTOs in the Castkit::DataObjects
namespace.
π§ͺ List Serializers
castkit list serializers
Lists all serializer classes and their source origin.
π§° Example Usage
castkit generate dataobject Product name:string price:float
castkit generate contract ProductInput name:string
castkit list types
castkit list validators
The CLI is designed to provide a familiar Rails-like generator experience, tailored for Castkitβs data-first architecture.
Testing
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/)
Compatibility
- Ruby 2.7+
- Zero dependencies (uses core Ruby)
License
MIT. See LICENSE.
π Credits
Created with β€οΈ by Nathan Lucas