hash_serializable

Send.cr CI GitHub release GitHub commits since latest release (by SemVer)

It can be useful to be able to serialize and deserialize between hashes and objects the same way that one can between JSON and objects and YAML and objects. This implementation is aiming to be feature-consistent with JSON::Serializable and YAML::Serializable, while working with hashes.

The Hash::Serializable module automatically generates methods for serialization when included.

Example

require "hash_serializable"

class Note
  include Hash::Serializable

  property message : String = "DEFAULT"
end

class Location
  include Hash::Serializable

  @[Hash::Field(key: "lat")]
  property latitude : Float64

  @[Hash::Field(key: "lon")]
  property longitude : Float64

  property note : Note
end

class House
  include Hash::Serializable

  property address : String
  property location : Location?
  property note : Note
end

arg = {
  "note" => {
    "message" => "Nice Address",
  },
  "address"  => "Crystal Road 1234",
  "location" => {
    "lat"  => 12.3,
    "lon"  => 34.5,
    "note" => {
      "message" => "hmmmm",
    },
  },
}
house = House.from_hash(arg)

house.is_a?(House).should be_true
house.address.should eq "Crystal Road 1234"
house.location.is_a?(Location).should be_true
house.location.not_nil!.latitude.should eq 12.3
house.location.not_nil!.longitude.should eq 34.5
house.note.message.should eq "Nice Address"
house.location.not_nil!.note.message.should eq "hmmmm"
house.to_hash.should eq arg

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      hash_serializable:
        github: your-github-user/hash_serializable
  2. Run shards install

Usage

Including Hash::Serializable will create #to_hash and self.from_hash methods on the current class, and a constructor which takes a Hash. By default, self.from_hash will deserialize a Hash into an instance of the object that it is passed to, according to the definition of the class, and #to_hash will serialize the class into a Hash containing the value of every instance variable, the keys being the instance variable names.

It will descend through a nested class structure, where variables in one class point to objects that, in turn, have instance variables. It should also deal correctly with type unions.

To change how individual instance variables are parsed and serialized, the annotation Hash::Field can be placed on the instance variable. Annotating property, getter, and setter macros is also allowed.

require "hash_serializable"

struct A
  include Hash::Serializable

  @[Hash::Field(key: "my_key")]
  getter a : Int32?
end

Hash::Field properties:

Deserialization respects default values of variables.

Extensions: Hash::Serializable::Strict and Hash::Serializable::Unmapped

If the Hash::Serializable::Strict module is included, unknown properties in the Hash document will raise an exception. By default the unknown properties are silently ignored.

If the Hash::Serializable::Unmapped module is included, unknown properties in the Hash will be stored in a hash with an appropriate type signature. On serialization, any keys inside json_unmapped will be serialized into the hash, as well.

require "hash_serializable"

struct A
  include Hash::Serializable
  include Hash::Serializable::Unmapped
  @a : Int32
end

a = A.from_json(%({"a":1,"b":2})) # => A(@json_unmapped={"b" => 2_i64}, @a=1)
a.to_json                         # => {"a":1,"b":2}

Casting

Hash::Serializable can automatically convert values from one type to another. For example, if one has a RequestParams object that serialized the query parameters in an HTTP request, and one wanted to define a user_id field that was an integer, one might do something like this:

struct RequestParams
  use Hash::Serializable

  @[Hash::Field(cast: :to_i)]
  getter user_id : Int32
end

params = RequestParams.new({"user_id" => "123"})
pp params # >    #<RequestParams:0x7f2653e2f080 @user_id=123 >

One can also provide a proc to do the value conversion:

struct MyObj
  use Hash::Serializable

  @[Hash::Field(
    key: "number",
    cast: ->(x : String | Int::Signed | Int::Unsigned | Float::Primitive) do
      BigInt.new(x) ** 2
    end)]
  getter square : BigInt
end

obj = MyObj.new({"number" => 12345678901234567890})
pp obj # >    #<MyObj:0x7fc5adbe5e80 @square=152415787532388367501905199875019052100>

The library does not yet support having procs or methods which will cast back to the original type.

Development

If you want to help to fix bugs, clean up the code or the documentation, or help with additional features, fork the repository, and work in your fork, and then create a pull request against this repository.

Check issues for any known problems, and create new issues if you have problems or questions or feature requests.

Contributing

  1. Fork it (https://github.com/wyhaines/hash_serializable/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

GitHub code size in bytes GitHub issues