Konstruo
Konstruo maps JSON, hashes, and Rails params into typed Ruby objects with:
- Required field validation
- Runtime type checks
- Custom key mapping (for example
userId->user_id) - Value transformation hooks (mappers)
- Nested object support (including arrays of nested objects)
- Inherited field definitions for mapper subclasses
- Optional strict mode to reject unknown input keys
Installation
Add to your Gemfile:
gem 'konstruo'Then install:
bundle installOr install directly:
gem install konstruoQuick Start
require 'konstruo'
class Address < Konstruo::Mapper
field :street, String, required: true
field :city, String, required: true
end
class Person < Konstruo::Mapper
field :name, String, required: true
field :age, Integer
field :is_active, Konstruo::Boolean, required: true
field :address, Address, required: true
field :tags, [String]
# Map external keys to ruby-style attribute names
field :user_id, Integer, required: true, custom_name: 'userId'
# Transform value before type validation/assignment
field :signup_date, Date, custom_name: 'signupDate', mapper: ->(v) { Date.parse(v) }
end
json = <<~JSON
{
"name": "John Doe",
"age": 30,
"is_active": true,
"address": { "street": "123 Main St", "city": "New York" },
"tags": ["ruby", "rails"],
"userId": 42,
"signupDate": "2023-08-31"
}
JSON
person = Person.from_json(json)
person.name # => "John Doe"
person.user_id # => 42
person.signup_date # => #<Date: 2023-08-31 ...>
person.address.city # => "New York"Inheritance
Mapper fields are inherited by subclasses:
class BasePayload < Konstruo::Mapper
field :request_id, String, required: true, custom_name: 'requestId'
end
class CreateUserPayload < BasePayload
field :name, String, required: true
end
payload = CreateUserPayload.from_hash(requestId: 'abc-123', name: 'Jane')
payload.request_id # => "abc-123"
payload.name # => "Jane"Defining Fields
Field API:
field(name, type, required: false, nullable: nil, custom_name: nil, mapper: nil, error_message: nil)Options:
-
name(Symbol): Ruby attribute name. -
type(Classor[Class]): Expected value type. -
required(Boolean): RaisesKonstruo::ValidationErrorwhen missing. -
nullable(Boolean, optional): Controls whether a present key can havenilvalue. -
custom_name(String): External key name to read from input. -
mapper(Proc): Converts raw input value before assignment. -
error_message(String): Custom validation error message.
Supported type patterns:
- Primitive/class values:
String,Integer,Date, etc. - Boolean:
Konstruo::Boolean - Nested mapper: any subclass of
Konstruo::Mapper - Array of primitives:
[String],[Integer], etc. - Array of nested mappers:
[Address]
Array type declarations must contain exactly one element class (for example [String]).
nullable default behavior:
- If
required: trueandnullableis omitted,nullabledefaults tofalse. - If
required: falseandnullableis omitted,nullabledefaults totrue.
Parsing Input
Konstruo supports three entry points:
YourMapper.from_json(json_string)YourMapper.from_hash(hash)YourMapper.from_params(action_controller_params)
All return an instance of your mapper class.
Notes:
-
from_hashaccepts string or symbol keys. -
from_jsonexpects a JSON object at the root and raisesKonstruo::ValidationErrorotherwise.
person = Person.from_hash(
name: 'Jane',
is_active: true,
address: { street: '42 Broadway', city: 'NYC' },
userId: 7
)Validation Behavior
Required fields
Missing required fields raise:
Konstruo::ValidationErrorDefault message format:
Missing required field: field_name
Nullability
When a key is present with nil value:
-
nullable: trueallows it. -
nullable: falseraises:
Field cannot be nil: field_name
Type errors
Type mismatches raise Konstruo::ValidationError with details like:
Expected Integer for field: age, got String
Expected String for field: friends[0], got Integer
Expected Boolean for field: is_active, got String
Konstruo::Boolean only accepts real booleans (true or false).
Nested error paths
Errors coming from nested mappers are prefixed with their full path:
address: Street is required.
addresses[0]: Street is required.
Missing required field: address.city
Strict unknown key mode
Enable strict mode in a mapper to reject keys that are not declared with field:
class StrictPerson < Konstruo::Mapper
strict_unknown_keys
field :name, String, required: true
end
StrictPerson.from_hash(name: 'Jane', extra: 'value')
# => raises Konstruo::ValidationError: Unknown fields: extraBy default, unknown keys are ignored.
Strict mode is inherited by subclasses.
You can explicitly disable it with strict_unknown_keys(false).
Custom mappers
Mapper lambdas are executed as provided. If they raise (for example Date.parse), that error bubbles up.
Custom error messages
error_message: is used for both missing required fields and type errors on that field.
Sorbet Integration
field is defined dynamically at runtime, so static typing for generated accessors comes from Tapioca DSL RBIs.
Konstruo exports a Tapioca DSL compiler from the gem path tapioca/dsl/compilers, so consumer apps pick it up automatically when Konstruo is in the bundle.
Run:
bundle exec tapioca dslThe compiler generates typed accessors for mapper fields, so you do not need to manually write repeated sig + attr_accessor declarations for each field.
Rails Params Support
Use from_params when parsing ActionController::Parameters:
def create
person = Person.from_params(params.require(:person).permit!)
# ...
endDevelopment
bin/setup
bundle exec rake testUseful commands:
-
bin/consolefor interactive experimentation -
bundle exec rake installto install the gem locally -
bundle exec rake releaseto tag and publish
Contributing
Issues and pull requests are welcome: https://github.com/DashBrains/konstruo
License
MIT: LICENSE.txt