fast-jsonapi-serializer
Version, currently 0.1.02 versions
- 0.2.0latestJun 23, 2021
- 0.1.0not indexedNov 10, 2023
github.com/mjeffrey18/fast-jsonapi-serializer
FastJSONAPISerializer is a fast, flexible and simple JSON-API serializer for crystal
Nothing has been indexed for 0.1.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
fast-jsonapi-serializer:
github: mjeffrey18/fast-jsonapi-serializer
version: ~> 0.1.0Then run:
shards installshard.yml
No shard.yml has been indexed for 0.1.0. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
# FastJSONAPISerializer
 [](https://mjeffrey18.github.io/fast-jsonapi-serializer/) [](https://github.com/mjeffrey18/fast-jsonapi-serializer/releases)
Fast JSON-API Serializer is a fast, flexible and simple [JSON-API](https://jsonapi.org) serializer for crystal.
Refer to the full API [documentation](https://mjeffrey18.github.io/fast-jsonapi-serializer/)
## Why use it? 😅
- Works with any ORM or plain Crystal objects.
- Offers a very flexible API.
- Did I mention it was fast?
## Benchmarks 🚀
> **Spoiler** **~200%** faster!
*Compared to other JSON-API compliant alternatives. Sure, benchmarks are to be taken with a grain of salt...*
See `examples/benchmark.cr` for the full benchmark setup.
(Kitchen Sink) With various relationships and all API features used -
```
FastJSONAPISerializer 66.54k ( 15.03µs) (± 2.25%) 22.2kB/op fastest
JSONApiSerializer 34.32k ( 29.14µs) (± 2.49%) 33.0kB/op 1.94× slower
```
Single object with 1 attribute
```
FastJSONAPISerializer 881.46k ( 1.13µs) (± 1.98%) 1.47kB/op fastest
JSONApiSerializer 669.06k ( 1.49µs) (± 2.65%) 1.44kB/op 1.32× slower
```
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
fast-jsonapi-serializer:
github: mjeffrey18/fast-jsonapi-serializer
```
2. Run `shards install`
## Setup
Require the shard in your project.
```crystal
require "fast-jsonapi-serializer"
```
## Usage
### Quick Introduction
Considering a model/resource (ORM or plain crystal class)
```crystal
class Restaurant
property name
def initialize(@name = "big burgers")
end
end
```
Create a serializer which inherits from `FastJSONAPISerializer::Base(YourResourceClass)`
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
end
```
Use the `serialize` API to to build a `JSON-API` compatible string
#### Single Resource
```crystal
resource = Restaurant.new
RestaurantSerializer.new(resource).serialize
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"name": "big burgers"
}
}
}
```
#### Resource Collection
```crystal
resources = [Restaurant.new, Restaurant.new]
RestaurantSerializer.new(resources).serialize
```
Example above produces this output (made readable for docs):
```json
{
"data": [
{
"id": "1",
"type": "restaurant",
"attributes": {
"name": "big burgers"
}
},
{
"id": "2",
"type": "restaurant",
"attributes": {
"name": "big sandwiches"
}
}
]
}
```
### Type
By default, the JSON-API type key will be the *snake_case* name of the resource class i.e. `AdminUser -> "admin_user"`.
You can override this behaviour by setting the `type(String)` macro.
```crystal
class AdminUserSerializer < FastJSONAPISerializer::Base(AdminUser)
type "user"
attribute :name
end
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "user",
"attributes": {
"name": "Joe"
}
}
}
```
### ID
Your resource class should have an id instance method or getter to populate the JSON `id` field of the resource.
#### Supported ID's
- Integer
- String
- UUID
- Nil
If the resource does not respond to `id` the JSON `id` value will become `null` - giving a little more flexibility, although not advised or complaint with the `JSON-API` standard.
> IMPORTANT - As per the [JSON-API](https://jsonapi.org) standard, we always convert the id to a string.
Example without and id below;
```crystal
class Restaurant
property name
def initialize(@name = "big burgers")
end
end
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
end
RestaurantSerializer.new(Restaurant.new).serialize
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": null,
"type": "restaurant",
"attributes": {
"name": "big burgers"
}
}
}
```
### Attributes
The attributes API is very flexible.
**Single** `attribute`
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
attribute :street
end
```
**Multiple** `attributes`
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attributes :name, :street
end
```
**Mixed**
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attributes :name, :street
attribute :post_code
end
```
**Serializer methods**
You can also list `attributes` which are on the serializer class;
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
attribute :custom_method_on_serializer
def custom_method_on_serializer(_object, _options)
123
end
def custom_method_on_serializer_two(object, options)
if options[:show_full]
object.full_data
else
object.data
end
end
end
```
#### Control the attribute JSON key name
Let's say you want to have different key name or case, you can pass this as a second argument `attribute`
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name, :FullName
end
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"FullName": "big burgers"
}
}
}
```
#### Conditional control of the attributes
**Attribute API**
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name, :FullName, if: :should_show_name
def should_show_name(object, _options)
object.has_full_name?
end
end
RestaurantSerializer.new(Restaurant.new).serialize
```
OR
Use the `serialize(options: ...)` API to control the attributes
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name, :FullName, if: :should_show_name
def should_show_name(object, options)
object.has_full_name? && options[:allow_name]
end
end
RestaurantSerializer.new(Restaurant.new).serialize(
options: {:allow_name => true}
)
```
**Serialize API**
We can have any number of attributes which can be excluded on demand.
Use the `serialize(except: ...)` API to control the attributes
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name, :address, :post_code
end
RestaurantSerializer.new(Restaurant.new).serialize(
except: %i(name postcode)
)
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"address": "somewhere cool"
}
}
}
```
### Relations
The following relationships are supported:
- `belongs_to`
- `has_many`
- `has_one`
Given a model which has various associations like follows:
```crystal
class Restaurant
property id : String,
name : String,
address : Nil | Address = nil,
post_code : Nil | PostCode = nil,
rooms : Array(Room) = [] of Room
def initialize(@id, @name = "big burgers")
end
def tables
[Table.new(1), Table.new(2), Table.new(3)]
end
end
```
You can define the serializer relationships
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
belongs_to :address, AddressSerializer
has_one :post_code, PostCodeSerializer
has_many :rooms, RoomSerializer
has_many :tables, TableSerializer, :Tables # here we can override the name (optional)
end
# Or if you prefer a more explicit approach
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
attribute :name
belongs_to :address, serializer: AddressSerializer
has_one :post_code, serializer: PostCodeSerializer
has_many :rooms, serializer: RoomSerializer
has_many :tables, serializer: TableSerializer, key: :Tables
end
```
Make sure to use the `serialize(includes: ...)` API to include the relations:
```crystal
# build all associations
resource = Restaurant.new
resource.address = Address.new
resource.post_code = PostCode.new
room = Room.new(1)
room.tables = [Table.new(1), Table.new(2)]
resource.rooms = [room]
RestaurantSerializer.new(resource).serialize(
includes: {
:address => [:address],
:post_code => [:post_code],
:tables => {:room => [:room]}, # notice nested associations also
}
)
```
> **IMPORTANT** - Relationships do nothing unless requested via the `serialize(includes: ...)` API
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"name": "big burgers"
},
"relationships": {
"address": {
"data": {
"id": "101",
"type": "address"
}
},
"post_code": {
"data": {
"id": "101",
"type": "post_code"
}
},
"Tables": {
"data": [
{
"id": "1",
"type": "table"
},
{
"id": "2",
"type": "table"
},
{
"id": "3",
"type": "table"
}
]
}
}
},
"included": [
{
"id": "101",
"type": "address",
"attributes": {
"street": "some street"
}
},
{
"id": "101",
"type": "post_code",
"attributes": {
"code": "code 24"
}
},
{
"id": "1",
"type": "room",
"attributes": {
"name": "1-name"
},
"relationships": {}
},
{
"id": "1",
"type": "table",
"attributes": {
"number": 1
},
"relationships": {
"room": {
"data": {
"id": "1",
"type": "room"
}
}
}
},
{
"id": "2",
"type": "room",
"attributes": {
"name": "2-name"
},
"relationships": {}
},
{
"id": "2",
"type": "table",
"attributes": {
"number": 2
},
"relationships": {
"room": {
"data": {
"id": "2",
"type": "room"
}
}
}
},
{
"id": "3",
"type": "room",
"attributes": {
"name": "3-name"
},
"relationships": {}
},
{
"id": "3",
"type": "table",
"attributes": {
"number": 3
},
"relationships": {
"room": {
"data": {
"id": "3",
"type": "room"
}
}
}
}
]
}
```
### Meta
You can add meta details to the JSON response payload.
**Serialize API**
Use the `serialize(meta: ...)` API to control the meta attributes
```crystal
RestaurantSerializer.new(Restaurant.new).serialize(
meta: {:page => 0, :limit => 50}
)
```
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"name": "big burgers"
}
},
"meta": {
"page": 0,
"limit": 50
}
}
```
**.meta class method**
You can define default meta attributes as a class method on the serializer.
Using the `serialize(meta: ...)` API you can **merge** or **override** the default meta attributes
```crystal
class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
def self.meta(options)
{
:status => "ok"
} of Symbol => FastJSONAPISerializer::MetaAny
end
end
RestaurantSerializer.new(Restaurant.new).serialize(
meta: {:page => 0, :limit => 50}
)
```
> Note - `FastJSONAPISerializer::MetaAny` -> (JSON::Any::Type | Int32)
Example above produces this output (made readable for docs):
```json
{
"data": {
"id": "1",
"type": "restaurant",
"attributes": {
"name": "big burgers"
}
},
"meta": {
"status": "ok",
"page": 0,
"limit": 50
}
}
```
### Serialize API
We covered all the options in the previous examples but this shows all available options.
- `except` - array of fields which should be excluded
- `includes` - definition of relation that should be included
- `options` - options that will be passed to methods defined for `if` attribute options and `.meta(options)`
- `meta` - meta attributes to be added under `"meta"` key at root level, merged into default `.meta`
Kitchen sink example:
```crystal
RestaurantSerializer.new(resource).serialize(
except: %i(name),
includes: {
:address => [:address],
:post_code => [:post_code],
:tables => {:room => [:room]},
},
meta: {:page => 0, :limit => 50},
options: {:show_rating => true}
)
```
### Inheritance
You can DRY your serializers with inheritance - just add required attributes and/or associations in the subclasses.
```crystal
class UserSerializer < Serializer::Base(User)
attributes :name, :age
end
class FullUserSerializer < UserSerializer
attributes :email, :created_at
has_many :identities, IdentitySerializer
end
```
## TODO
- Allow Proc based conditional attributes
- Allow Proc based conditional relationships
- Allow global key case-change option
- Allow links meta data
- Add safety checks for inputs and bad data
## Contributing
1. Fork it (<https://github.com/mjeffrey18/fast-jsonapi-serializer/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
## Acknowledgements
This project was based on concepts gather from another amazing open source shard - [serializer](https://github.com/imdrasil/serializer)
Thank you so much for the inspiration!
--
I did use this shard as a bench comparison, but with good intentions. Big shout out to [jsonapi-serializer-cr](https://github.com/andersondanilo/jsonapi-serializer-cr)
This project is awesome and has helped me build projects, great work!
## Contributors
- [Marc Jeffrey](https://github.com/mjeffrey18) - creator and maintainer
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.1.0- Tagged
- Nov 10, 2023
- Commit
98849b34938f- Indexed
- not yet
Dependents
No indexed shard depends on this one yet.
Repository
github.com/mjeffrey18/fast-jsonapi-serializer
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 14, 2026
- Synced
- Aug 13, 2026
- Versions
- 2