JSON to Crystal: JSON::Serializable classes
Crystal has JSON support in its standard library. Including the JSON::Serializable module in a class generates from_json and to_json at compile time, based on the declared property types. This JSON to Crystal generator writes those classes from a sample, so you get compile-time checked models with no runtime reflection and no extra shards.
Example
{ "id": 42, "title": "Write docs", "done": false,
"owner": { "name": "Ada", "email": null },
"subtasks": [ { "title": "Outline", "estimate": 2 }, { "title": "Draft" } ] }
require "json"
class Task
include JSON::Serializable
property id : Int32
property title : String
property done : Bool
property owner : Owner
property subtasks : Array(Subtask)
end
class Subtask
include JSON::Serializable
property title : String
property estimate : Int32?
end
Int32? is Crystal shorthand for Int32 | Nil; missing or null keys decode as nil. The output above is shown with blank lines between properties removed.
How types are inferred
Crystal has real union types, and the generator uses them. Decimals become Float64, booleans Bool, arrays Array(T), and an array that mixes numbers and strings gets an alias such as alias X = Int32 | String. A key that is missing from some objects, or sometimes null, becomes nilable with ?. The compiler then forces you to handle the nil case before using the value, so the generated types turn gaps in your data into compile errors rather than runtime crashes. Paste a representative array of records to get the most accurate unions.
Using the classes
task = Task.from_json(File.read("task.json"))
puts task.subtasks.first.title
puts task.to_json
If the JSON does not match, from_json raises JSON::SerializableError naming the property, which makes API changes easy to diagnose.
Things to adjust
- Integer size: whole numbers are typed
Int32. For IDs or timestamps that can exceed about 2.1 billion, change them toInt64; otherwise parsing overflows. - Dates are kept as
String. Crystal'sTimeworks with JSON through a converter:@[JSON::Field(converter: Time::Format.new("%FT%T%z"))]. - Renaming: keys that are not valid Crystal names, or that you want in snake_case, use
@[JSON::Field(key: "userName")]above the property. - Unknown keys are ignored by default; include
JSON::Serializable::Strictto raise on them. - Use
getterinstead ofpropertyfor read-only models.
Crystal's syntax is close to Ruby's; for a Ruby version of the same models see JSON to Ruby. Validate your input with the JSON Validator. Everything runs in your browser.
Parsing arrays, maps and loose JSON
The generated class describes one record, but the same class works for other top-level shapes without any changes:
tasks = Array(Task).from_json(File.read("tasks.json"))
by_id = Hash(String, Task).from_json(%({"a1": {...}}))
any = JSON.parse(raw) # JSON::Any when no class fits
Use Array(Task) when the API returns a list and Hash(String, Task) when records are keyed by an ID. JSON.parse returns an untyped JSON::Any, which is handy for exploring a response but gives up the compile-time checks that make the generated classes worthwhile.