Skip to content

Commit

Permalink
Adds support for JSON::Serializable (#253)
Browse files Browse the repository at this point in the history
* Support JSON::Serializable

* Field specific JSON serrialization options

* Update readme

* Add JSON section to README

* Change error.to_s back to if/else
  • Loading branch information
Blacksmoke16 authored and robacarp committed Jul 17, 2018
1 parent d6a758b commit 4c94445
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 186 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM crystallang/crystal:0.25.0
FROM crystallang/crystal:0.25.1

RUN apt-get update -qq && apt-get install -y --no-install-recommends libpq-dev libsqlite3-dev libmysqlclient-dev

Expand Down
190 changes: 187 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ end
# There are various ways to instantiate a new object:
namedTuple = Post.new(name: "Name", body: "I am the body") # .new NamedTuple
hash = Post.new({name: "Name", body: "I am the body"}.to_h) # .new Hash
fromJson = Post.from_json(JSON.parse(%({"name": "Name", "body": "I am the body"}))).as(Post) # .from_json JSON
jsonArray = Post.from_json(JSON.parse(%([{"name": "Post1", "body": "I am the body for post1"},{"name": "Post2", "body": "I am the body for post2"}]))).as(Array(Post)) # .from_json array of JSON
fromJson = Post.from_json(JSON.parse(%({"name": "Name", "body": "I am the body"}))) # .from_json JSON
jsonArray = Array(Post).from_json(JSON.parse(%([{"name": "Post1", "body": "I am the body for post1"},{"name": "Post2", "body": "I am the body for post2"}]))) # .from_json array of JSON
# Can also use .create to automatically instantiate and save a model
Post.create(name: "First Post", body: "I get saved automatically") # Instantiates and saved the post
```
**Note: When using `.from_json` you must specify the type that will be returned. I.e. single object or an array of objects.**
**Note: When using `.from_json/.to_json` see [JSON::Serialization](https://crystal-lang.org/api/0.25.1/JSON/Serializable.html) for more details.**

You can disable the timestamps for SqlLite since TIMESTAMP is not supported for this database:

Expand Down Expand Up @@ -718,6 +718,190 @@ class Post < Granite::Base
end
```

### JSON Support

A few handy things that come from the `JSON::Serializable` support. See [JSON::Serializable Docs](https://crystal-lang.org/api/0.25.1/JSON/Serializable.html) for more information.

#### JSON::Field

Allows for control over the serialization and deserialization of instance variables.

```Crystal
class Foo < Granite::Base
adapter mysql
table_name foos
field name : String
field password : String, json_options: {ignore: true} # skip this field in serialization and deserialization
field age : Int32, json_options: {key: "HowOldTheyAre"} # the value of the key in the json object
field todayBirthday : Bool, json_options: {emit_null: true} # emits a null value for nilable property
field isNil : Bool
end
```

`foo = Foo.from_json(%({"name": "Granite1", "HowOldTheyAre": 12, "password": "12345"}))`

```Crystal
#<Foo:0x1e5df00
@_current_callback=nil,
@age=12,
@created_at=nil,
@destroyed=false,
@errors=[],
@id=nil,
@isNil=nil,
@name="Granite1",
@new_record=true,
@password=nil,
@todayBirthday=nil,
@updated_at=nil>
```

`foo.to_json`

```JSON
{
"name":"Granite1",
"HowOldTheyAre":12,
"todayBirthday":null
}
```

Notice how `isNil` is omitted from the JSON output since it is Nil. If you wish to always show Nil instance variables on a class level you can do:

```Crystal
@[JSON::Serializable::Options(emit_nulls: true)]
class Foo < Granite::Base
adapter mysql
table_name foos
field name : String
field age : Int32
end
```

This would be functionally the same as adding `json_options: {emit_null: true}` on each property.

#### after_initialize

This method gets called after `from_json` is done parsing the given JSON string. This allows you to set other fields that are not in the JSON directly or that require some more logic.

```Crystal
class Foo < Granite::Base
adapter mysql
table_name foos
field name : String
field age : Int32
field date_added : Time
def after_initialize
@date_added = Time.utc_now
end
end
```

`foo = Foo.from_json(%({"name": "Granite1"}))`

```Crystal
<Foo:0x55c77d901ea0
@_current_callback=nil,
@age=0,
@created_at=nil,
@destroyed=false,
@date_added=2018-07-17 01:08:28.239807000 UTC,
@errors=[],
@id=nil,
@name="Granite",
@new_record=true,
@updated_at=nil>
```

#### JSON::Serializable::Unmapped

If the JSON::Serializable::Unmapped module is included, unknown properties in the JSON document will be stored in a Hash(String, JSON::Any). On serialization, any keys inside `json_unmapped` will be serialized appended to the current JSON object.

```Crystal
class Foo < Granite::Base
include JSON::Serializable::Unmapped
adapter mysql
table_name foos
field name : String
field age : Int32
end
```

`foo = Foo.from_json(%({"name": "Granite1", "age": 12, "foobar": true}))`

```Crystal
<Foo:0x55efba40ff00
@_current_callback=nil,
@age=12,
@created_at=nil,
@destroyed=false,
@errors=[],
@id=nil,
@json_unmapped={"foobar" => true},
@name="Granite1",
@new_record=true,
@updated_at=nil>
```

`foo.to_json`

```JSON
{
"name": "Granite",
"age": 12,
"foobar": true
}
```

#### on_to_json

Allows doing additional manipulations before returning from `to_json`.

```Crystal
class Foo < Granite::Base
adapter mysql
table_name foos
field name : String
field age : Int32
def on_to_json(json : JSON::Builder)
json.field("years_young", @age)
end
end
```

`foo = Foo.from_json(%({"name": "Granite1", "age": 12}))`

```Crystal
<Foo:0x55eb12bd2f00
@_current_callback=nil,
@age=12,
@created_at=nil,
@destroyed=false,
@errors=[],
@id=nil,
@name="Granite1",
@new_record=true,
@updated_at=nil>
```

`foo.to_json`

```JSON
{
"name": "Granite",
"age": 12,
"years_young": 12
}
```

### Migration

- `migrator` provides `drop`, `create` and `drop_and_create` methods
Expand Down
17 changes: 0 additions & 17 deletions spec/granite/transactions/create_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,6 @@ module {{adapter.capitalize.id}}
parent.persisted?.should be_false
end

it "takes JSON::Any" do
json_str = %({"name": "json::anyReview", "downvotes": 99, "upvotes": 2, "sentiment": 1.23, "interest": 4.56, "published": true})
review_json = JSON.parse(json_str)

review_json.is_a?(JSON::Any).should be_true

review = Review.create(review_json)
review.name.should eq "json::anyReview"
review.downvotes.should eq 99_i32
review.upvotes.should eq 2_i64
review.sentiment.should eq 1.23_f32
review.interest.should eq 4.56_f64
review.published.should eq true
review.created_at.to_s.should eq Time.utc_now.to_s
review.persisted?.should be_true
end

it "doesn't have a race condition on IDs" do
channel = Channel(Int64).new

Expand Down
Loading

0 comments on commit 4c94445

Please sign in to comment.