JSON to Ruby with dry-struct
Ruby's JSON.parse returns hashes with string keys and no guarantees about their contents. This JSON to Ruby generator produces typed models built on the dry-rb gems: each object shape becomes a Dry::Struct subclass, and every attribute has a type from dry-types. Constructing a struct with a wrong type raises an error immediately, which keeps bad data out of the rest of your application.
Example
{ "id": 42, "title": "Write docs", "done": false,
"owner": { "name": "Ada", "email": null },
"subtasks": [ { "title": "Outline", "estimate": 2 }, { "title": "Draft" } ] }
require 'json'
require 'dry-types'
require 'dry-struct'
module Types
include Dry.Types(default: :nominal)
Integer = Strict::Integer
String = Strict::String
Bool = Strict::Bool
Nil = Strict::Nil
end
class Subtask < Dry::Struct
attribute :title, Types::String
attribute :estimate, Types::Integer.optional
end
class Task < Dry::Struct
attribute :id, Types::Integer
attribute :title, Types::String
attribute :done, Types::Bool
attribute :owner, Owner
attribute :subtasks, Types.Array(Subtask)
end
Parsing JSON with the helpers
With Types only off, each struct gets class methods from_dynamic! (from a parsed hash) and from_json! (from a string), plus to_dynamic and to_json. They map JSON keys to attributes and apply the strict types, raising an error when the data does not match:
task = Task.from_json!(File.read("task.json"))
puts task.subtasks.first.title
puts task.to_json
The generated file notes that it "may look unusually verbose for Ruby"; that verbosity is the validation.
How types are inferred
Whole numbers use Types::Integer, decimals a Types::Double that accepts floats and integers, strings Types::String and booleans Types::Bool. Arrays are Types.Array(T) and nested objects their own Dry::Struct classes. Attributes missing from some objects or sometimes null get .optional. Because every type is Strict, a string where an integer is expected is rejected rather than coerced, which is the behavior you want at an API boundary.
Notes
- Add
dry-structanddry-typesto your Gemfile. - Attribute names that would clash with methods already defined on
Dry::Structare renamed; for example, anamekey becomesowner_namein theOwnerstruct, while the helpers still read the originalnamekey. - Dates are typed as strings; parse them with
Time.iso8601where needed. - In Rails you might prefer ActiveModel or plain POROs; the attribute list is still a good starting point.
Format the result with the Ruby Formatter. For Crystal, a language with Ruby-like syntax and compile-time types, see JSON to Crystal. Generation runs in your browser.