module JSON

Overview

The JSON module allows parsing and generating JSON documents.

General type-safe interface

The general type-safe interface for parsing JSON is to invoke T.from_json on a target type T and pass either a String or IO as an argument.

require "json"

json_text = %([1, 2, 3])
Array(Int32).from_json(json_text) # => [1, 2, 3]

json_text = %({"x": 1, "y": 2})
Hash(String, Int32).from_json(json_text) # => {"x" => 1, "y" => 2}

Serializing is achieved by invoking to_json, which returns a String, or to_json(io : IO), which will stream the JSON to an IO.

require "json"

[1, 2, 3].to_json            # => "[1,2,3]"
{"x" => 1, "y" => 2}.to_json # => "{\"x\":1,\"y\":2}"

Most types in the standard library implement these methods. For user-defined types you can define a self.new(pull : JSON::PullParser) for parsing and to_json(builder : JSON::Builder) for serializing. The following sections show convenient ways to do this using either JSON.mapping or JSON::Serializable.

NOTE JSON object keys are always strings but they can still be parsed and deserialized to other types. To deserialize, define a T.from_json_object_key?(key : String) : T? method, which can return nil if the string can't be parsed into that type. To serialize, define a to_json_object_key : String method can be serialized that way. All integer and float types in the standard library can be deserialized that way.

require "json"

json_text = %({"1": 2, "3": 4})
Hash(Int32, Int32).from_json(json_text) # => {1 => 2, 3 => 4}

{1.5 => 2}.to_json # => "{\"1.5\":2}"

Parsing and generating with JSON.mapping

Use JSON.mapping to define how an object is mapped to JSON, making it the recommended easy, type-safe and efficient option for parsing and generating JSON. Refer to that module's documentation to learn about it.

Parsing with JSON.parse

JSON.parse will return an Any, which is a convenient wrapper around all possible JSON types, making it easy to traverse a complex JSON structure but requires some casts from time to time, mostly via some method invocations.

require "json"

value = JSON.parse("[1, 2, 3]") # : JSON::Any

value[0]              # => 1
typeof(value[0])      # => JSON::Any
value[0].as_i         # => 1
typeof(value[0].as_i) # => Int32

value[0] + 1       # Error, because value[0] is JSON::Any
value[0].as_i + 10 # => 11

JSON.parse can read from an IO directly (such as a file) which saves allocating a string:

require "json"

json = File.open("path/to/file.json") do |file|
  JSON.parse(file)
end

Parsing with JSON.parse is useful for dealing with a dynamic JSON structure but is slower than using JSON.mapping.

Generating with JSON.build

Use JSON.build, which uses JSON::Builder, to generate JSON by emitting scalars, arrays and objects:

require "json"

string = JSON.build do |json|
  json.object do
    json.field "name", "foo"
    json.field "values" do
      json.array do
        json.number 1
        json.number 2
        json.number 3
      end
    end
  end
end
string # => %<{"name":"foo","values":[1,2,3]}>

Generating with to_json

to_json, to_json(IO) and to_json(JSON::Builder) methods are provided for primitive types, but you need to define to_json(JSON::Builder) for custom objects, either manually or using JSON.mapping.

Defined in:

json.cr
json/builder.cr
json/mapping.cr
json/serialization.cr

Class Method Summary

Macro Summary

Class Method Detail

def self.build(io : IO, indent = nil, &block) #

Writes JSON into the given IO. A JSON::Builder is yielded to the block.


[View source]
def self.build(indent = nil, &block) #

Returns the resulting String of writing JSON to the yielded JSON::Builder.

require "json"

string = JSON.build do |json|
  json.object do
    json.field "name", "foo"
    json.field "values" do
      json.array do
        json.number 1
        json.number 2
        json.number 3
      end
    end
  end
end
string # => %<{"name":"foo","values":[1,2,3]}>

[View source]
def self.parse(input : String | IO) : Any #

Parses a JSON document as a JSON::Any.


[View source]

Macro Detail

macro mapping(_properties_, strict = false) #

The JSON.mapping macro defines how an object is mapped to JSON.

Example

require "json"

class Location
  JSON.mapping(
    lat: Float64,
    lng: Float64,
  )
end

class House
  JSON.mapping(
    address: String,
    location: {type: Location, nilable: true},
  )
end

house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
house.address  # => "Crystal Road 1234"
house.location # => #<Location:0x10cd93d80 @lat=12.3, @lng=34.5>
house.to_json  # => %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}})

houses = Array(House).from_json(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
houses.size    # => 1
houses.to_json # => %([{"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}])

Usage

JSON.mapping must receive a series of named arguments, or a named tuple literal, or a hash literal, whose keys will define Crystal properties.

The value of each key can be a type. Primitive types (numbers, string, boolean and nil) are supported, as well as custom objects which use JSON.mapping or define a new method that accepts a JSON::PullParser and returns an object from it. Union types are supported, if multiple types in the union can be mapped from the JSON, it is undefined which one will be chosen.

The value can also be another hash literal with the following options:

  • type: (required) the type described above (you can use JSON::Any too)
  • key: the property name in the JSON document (as opposed to the property name in the Crystal code)
  • nilable: if true, the property can be Nil. Passing T? as a type has the same effect.
  • default: value to use if the property is missing in the JSON document, or if it's null and nilable was not set to true. If the default value creates a new instance of an object (for example [1, 2, 3] or SomeObject.new), a different instance will be used each time a JSON document is parsed.
  • emit_null: if true, emits a null value for nilable properties (by default nulls are not emitted)
  • converter: specify an alternate type for parsing and generation. The converter must define from_json(JSON::PullParser) and to_json(value, JSON::Builder) as class methods. Examples of converters are Time::Format and Time::EpochConverter for Time.
  • root: assume the value is inside a JSON object with a given key (see Object.from_json(string_or_io, root))
  • setter: if true, will generate a setter for the variable, true by default
  • getter: if true, will generate a getter for the variable, true by default
  • presence: if true, a {{key}}_present? method will be generated when the key was present (even if it has a null value), false by default

This macro by default defines getters and setters for each variable (this can be overrided with setter and getter). The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload.

The macro basically defines a constructor accepting a JSON::PullParser that reads from it and initializes this type's instance variables. It also defines a to_json(JSON::Builder) method by invoking to_json(JSON::Builder) on each of the properties (unless a converter is specified, in which case to_json(value, JSON::Builder) is invoked).

This macro also declares instance variables of the types given in the mapping.

If strict is true, unknown properties in the JSON document will raise a parse exception. The default is false, so unknown properties are silently ignored.


[View source]
macro mapping(**_properties_) #

This is a convenience method to allow invoking JSON.mapping with named arguments instead of with a hash/named-tuple literal.


[View source]