{"id":15713728,"url":"https://github.com/mjeffrey18/fast-jsonapi-serializer","last_synced_at":"2025-05-12T22:55:23.616Z","repository":{"id":49213286,"uuid":"369433088","full_name":"mjeffrey18/fast-jsonapi-serializer","owner":"mjeffrey18","description":"Fast JSON-API Serializer is a fast, flexible and simple JSON-API serializer for crystal","archived":false,"fork":false,"pushed_at":"2023-11-10T10:44:44.000Z","size":62,"stargazers_count":8,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-12T22:55:17.723Z","etag":null,"topics":["crystal","crystal-lang","crystal-language","json","json-api","serialization","serialization-library","serializer"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mjeffrey18.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-21T06:16:37.000Z","updated_at":"2024-12-07T19:01:28.000Z","dependencies_parsed_at":"2024-10-24T10:52:10.996Z","dependency_job_id":"f68a71ca-fcfc-49be-842f-0a27a2ea8102","html_url":"https://github.com/mjeffrey18/fast-jsonapi-serializer","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjeffrey18%2Ffast-jsonapi-serializer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjeffrey18%2Ffast-jsonapi-serializer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjeffrey18%2Ffast-jsonapi-serializer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjeffrey18%2Ffast-jsonapi-serializer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mjeffrey18","download_url":"https://codeload.github.com/mjeffrey18/fast-jsonapi-serializer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253837400,"owners_count":21971982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crystal","crystal-lang","crystal-language","json","json-api","serialization","serialization-library","serializer"],"created_at":"2024-10-03T21:33:07.148Z","updated_at":"2025-05-12T22:55:23.589Z","avatar_url":"https://github.com/mjeffrey18.png","language":"Crystal","readme":"# FastJSONAPISerializer\n\n![Build Status](https://github.com/mjeffrey18/fast-jsonapi-serializer/actions/workflows/ci.yml/badge.svg?branch=main) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://mjeffrey18.github.io/fast-jsonapi-serializer/) [![GitHub release](https://img.shields.io/github/release/mjeffrey18/fast-jsonapi-serializer.svg)](https://github.com/mjeffrey18/fast-jsonapi-serializer/releases)\n\n\nFast JSON-API Serializer is a fast, flexible and simple [JSON-API](https://jsonapi.org) serializer for crystal.\n\nRefer to the full API [documentation](https://mjeffrey18.github.io/fast-jsonapi-serializer/)\n\n## Why use it? 😅\n\n- Works with any ORM or plain Crystal objects.\n- Offers a very flexible API.\n- Did I mention it was fast?\n\n## Benchmarks 🚀\n\n\u003e **Spoiler** **~200%** faster!\n\n*Compared to other JSON-API compliant alternatives. Sure, benchmarks are to be taken with a grain of salt...*\n\nSee `examples/benchmark.cr` for the full benchmark setup.\n\n(Kitchen Sink) With various relationships and all API features used -\n\n```\nFastJSONAPISerializer  66.54k ( 15.03µs) (± 2.25%)  22.2kB/op        fastest\n    JSONApiSerializer  34.32k ( 29.14µs) (± 2.49%)  33.0kB/op   1.94× slower\n```\n\nSingle object with 1 attribute\n\n```\nFastJSONAPISerializer 881.46k (  1.13µs) (± 1.98%)  1.47kB/op        fastest\n    JSONApiSerializer 669.06k (  1.49µs) (± 2.65%)  1.44kB/op   1.32× slower\n```\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n```yaml\ndependencies:\n  fast-jsonapi-serializer:\n    github: mjeffrey18/fast-jsonapi-serializer\n```\n\n2. Run `shards install`\n\n## Setup\n\nRequire the shard in your project.\n\n```crystal\nrequire \"fast-jsonapi-serializer\"\n```\n\n## Usage\n\n### Quick Introduction\n\nConsidering a model/resource (ORM or plain crystal class)\n\n```crystal\nclass Restaurant\n  property name\n\n  def initialize(@name = \"big burgers\")\n  end\nend\n```\n\nCreate a serializer which inherits from `FastJSONAPISerializer::Base(YourResourceClass)`\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\nend\n```\n\nUse the `serialize` API to to build a `JSON-API` compatible string\n\n#### Single Resource\n\n```crystal\nresource = Restaurant.new\nRestaurantSerializer.new(resource).serialize\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"name\": \"big burgers\"\n    }\n  }\n}\n```\n\n#### Resource Collection\n\n```crystal\nresources = [Restaurant.new, Restaurant.new]\nRestaurantSerializer.new(resources).serialize\n\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"restaurant\",\n      \"attributes\": {\n        \"name\": \"big burgers\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"restaurant\",\n      \"attributes\": {\n        \"name\": \"big sandwiches\"\n      }\n    }\n  ]\n}\n```\n\n### Type\n\nBy default, the JSON-API type key will be the *snake_case* name of the resource class i.e. `AdminUser -\u003e \"admin_user\"`.\nYou can override this behaviour by setting the `type(String)` macro.\n\n```crystal\nclass AdminUserSerializer \u003c FastJSONAPISerializer::Base(AdminUser)\n  type \"user\"\n  attribute :name\nend\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"user\",\n    \"attributes\": {\n      \"name\": \"Joe\"\n    }\n  }\n}\n```\n\n### ID\n\nYour resource class should have an id instance method or getter to populate the JSON `id` field of the resource.\n\n#### Supported ID's\n\n- Integer\n- String\n- UUID\n- Nil\n\nIf 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.\n\n\u003e IMPORTANT - As per the [JSON-API](https://jsonapi.org) standard, we always convert the id to a string.\n\nExample without and id below;\n\n```crystal\nclass Restaurant\n  property name\n\n  def initialize(@name = \"big burgers\")\n  end\nend\n\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\nend\n\nRestaurantSerializer.new(Restaurant.new).serialize\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": null,\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"name\": \"big burgers\"\n    }\n  }\n}\n```\n\n### Attributes\n\nThe attributes API is very flexible.\n\n**Single** `attribute`\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\n  attribute :street\nend\n```\n\n**Multiple** `attributes`\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attributes :name, :street\nend\n```\n\n**Mixed**\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attributes :name, :street\n  attribute :post_code\nend\n```\n\n**Serializer methods**\n\nYou can also list `attributes` which are on the serializer class;\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\n  attribute :custom_method_on_serializer\n\n  def custom_method_on_serializer(_object, _options)\n    123\n  end\n\n  def custom_method_on_serializer_two(object, options)\n    if options[:show_full]\n      object.full_data\n    else\n      object.data\n    end\n  end\nend\n```\n\n#### Control the attribute JSON key name\n\nLet's say you want to have different key name or case, you can pass this as a second argument `attribute`\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name, :FullName\nend\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"FullName\": \"big burgers\"\n    }\n  }\n}\n```\n\n#### Conditional control of the attributes\n\n**Attribute API**\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name, :FullName, if: :should_show_name\n\n  def should_show_name(object, _options)\n    object.has_full_name?\n  end\nend\n\nRestaurantSerializer.new(Restaurant.new).serialize\n```\n\nOR\n\nUse the `serialize(options: ...)` API to control the attributes\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name, :FullName, if: :should_show_name\n\n  def should_show_name(object, options)\n    object.has_full_name? \u0026\u0026 options[:allow_name]\n  end\nend\n\nRestaurantSerializer.new(Restaurant.new).serialize(\n  options: {:allow_name =\u003e true}\n)\n```\n\n**Serialize API**\n\nWe can have any number of attributes which can be excluded on demand.\n\nUse the `serialize(except: ...)` API to control the attributes\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name, :address, :post_code\nend\n\nRestaurantSerializer.new(Restaurant.new).serialize(\n  except: %i(name postcode)\n)\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"address\": \"somewhere cool\"\n    }\n  }\n}\n```\n\n### Relations\n\nThe following relationships are supported:\n\n- `belongs_to`\n- `has_many`\n- `has_one`\n\nGiven a model which has various associations like follows:\n\n```crystal\nclass Restaurant\n  property id : String,\n    name : String,\n    address : Nil | Address = nil,\n    post_code : Nil | PostCode = nil,\n    rooms : Array(Room) = [] of Room\n\n  def initialize(@id, @name = \"big burgers\")\n  end\n\n  def tables\n    [Table.new(1), Table.new(2), Table.new(3)]\n  end\nend\n```\n\nYou can define the serializer relationships\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\n\n  belongs_to :address, AddressSerializer\n\n  has_one :post_code, PostCodeSerializer\n\n  has_many :rooms, RoomSerializer\n  has_many :tables, TableSerializer, :Tables # here we can override the name (optional)\nend\n\n# Or if you prefer a more explicit approach\n\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  attribute :name\n\n  belongs_to :address, serializer: AddressSerializer\n\n  has_one :post_code, serializer: PostCodeSerializer\n\n  has_many :rooms, serializer: RoomSerializer\n  has_many :tables, serializer: TableSerializer, key: :Tables\nend\n```\n\nMake sure to use the `serialize(includes: ...)` API to include the relations:\n\n```crystal\n# build all associations\nresource = Restaurant.new\nresource.address = Address.new\nresource.post_code = PostCode.new\nroom = Room.new(1)\nroom.tables = [Table.new(1), Table.new(2)]\nresource.rooms = [room]\n\nRestaurantSerializer.new(resource).serialize(\n  includes: {\n    :address   =\u003e [:address],\n    :post_code =\u003e [:post_code],\n    :tables    =\u003e {:room =\u003e [:room]}, # notice nested associations also\n  }\n)\n```\n\n\u003e **IMPORTANT** - Relationships do nothing unless requested via the `serialize(includes: ...)` API\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"name\": \"big burgers\"\n    },\n    \"relationships\": {\n      \"address\": {\n        \"data\": {\n          \"id\": \"101\",\n          \"type\": \"address\"\n        }\n      },\n      \"post_code\": {\n        \"data\": {\n          \"id\": \"101\",\n          \"type\": \"post_code\"\n        }\n      },\n      \"Tables\": {\n        \"data\": [\n          {\n            \"id\": \"1\",\n            \"type\": \"table\"\n          },\n          {\n            \"id\": \"2\",\n            \"type\": \"table\"\n          },\n          {\n            \"id\": \"3\",\n            \"type\": \"table\"\n          }\n        ]\n      }\n    }\n  },\n  \"included\": [\n    {\n      \"id\": \"101\",\n      \"type\": \"address\",\n      \"attributes\": {\n        \"street\": \"some street\"\n      }\n    },\n    {\n      \"id\": \"101\",\n      \"type\": \"post_code\",\n      \"attributes\": {\n        \"code\": \"code 24\"\n      }\n    },\n    {\n      \"id\": \"1\",\n      \"type\": \"room\",\n      \"attributes\": {\n        \"name\": \"1-name\"\n      },\n      \"relationships\": {}\n    },\n    {\n      \"id\": \"1\",\n      \"type\": \"table\",\n      \"attributes\": {\n        \"number\": 1\n      },\n      \"relationships\": {\n        \"room\": {\n          \"data\": {\n            \"id\": \"1\",\n            \"type\": \"room\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"room\",\n      \"attributes\": {\n        \"name\": \"2-name\"\n      },\n      \"relationships\": {}\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"table\",\n      \"attributes\": {\n        \"number\": 2\n      },\n      \"relationships\": {\n        \"room\": {\n          \"data\": {\n            \"id\": \"2\",\n            \"type\": \"room\"\n          }\n        }\n      }\n    },\n    {\n      \"id\": \"3\",\n      \"type\": \"room\",\n      \"attributes\": {\n        \"name\": \"3-name\"\n      },\n      \"relationships\": {}\n    },\n    {\n      \"id\": \"3\",\n      \"type\": \"table\",\n      \"attributes\": {\n        \"number\": 3\n      },\n      \"relationships\": {\n        \"room\": {\n          \"data\": {\n            \"id\": \"3\",\n            \"type\": \"room\"\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n### Meta\n\nYou can add meta details to the JSON response payload.\n\n**Serialize API**\n\nUse the `serialize(meta: ...)` API to control the meta attributes\n\n```crystal\nRestaurantSerializer.new(Restaurant.new).serialize(\n  meta: {:page =\u003e 0, :limit =\u003e 50}\n)\n```\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"name\": \"big burgers\"\n    }\n  },\n  \"meta\": {\n    \"page\": 0,\n    \"limit\": 50\n  }\n}\n```\n\n**.meta class method**\n\nYou can define default meta attributes as a class method on the serializer.\n\nUsing the `serialize(meta: ...)` API you can **merge** or **override** the default meta attributes\n\n```crystal\nclass RestaurantSerializer \u003c FastJSONAPISerializer::Base(Restaurant)\n  def self.meta(options)\n    {\n      :status =\u003e \"ok\"\n    } of Symbol =\u003e FastJSONAPISerializer::MetaAny\n  end\nend\n\nRestaurantSerializer.new(Restaurant.new).serialize(\n  meta: {:page =\u003e 0, :limit =\u003e 50}\n)\n```\n\n\u003e Note - `FastJSONAPISerializer::MetaAny` -\u003e (JSON::Any::Type | Int32)\n\nExample above produces this output (made readable for docs):\n\n```json\n{\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"restaurant\",\n    \"attributes\": {\n      \"name\": \"big burgers\"\n    }\n  },\n  \"meta\": {\n    \"status\": \"ok\",\n    \"page\": 0,\n    \"limit\": 50\n  }\n}\n```\n\n### Serialize API\n\nWe covered all the options in the previous examples but this shows all available options.\n\n- `except` - array of fields which should be excluded\n- `includes` - definition of relation that should be included\n- `options` - options that will be passed to methods defined for `if` attribute options and `.meta(options)`\n- `meta` - meta attributes to be added under `\"meta\"` key at root level, merged into default `.meta`\n\nKitchen sink example:\n\n```crystal\nRestaurantSerializer.new(resource).serialize(\n  except: %i(name),\n  includes: {\n    :address   =\u003e [:address],\n    :post_code =\u003e [:post_code],\n    :tables    =\u003e {:room =\u003e [:room]},\n  },\n  meta: {:page =\u003e 0, :limit =\u003e 50},\n  options: {:show_rating =\u003e true}\n)\n```\n\n### Inheritance\n\nYou can DRY your serializers with inheritance - just add required attributes and/or associations in the subclasses.\n\n```crystal\nclass UserSerializer \u003c Serializer::Base(User)\n  attributes :name, :age\nend\n\nclass FullUserSerializer \u003c UserSerializer\n  attributes :email, :created_at\n\n  has_many :identities, IdentitySerializer\nend\n```\n\n## TODO\n\n- Allow Proc based conditional attributes\n- Allow Proc based conditional relationships\n- Allow global key case-change option\n- Allow links meta data\n- Add safety checks for inputs and bad data\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/mjeffrey18/fast-jsonapi-serializer/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Acknowledgements\n\nThis project was based on concepts gather from another amazing open source shard - [serializer](https://github.com/imdrasil/serializer)\n\nThank you so much for the inspiration!\n\n--\n\nI 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)\n\nThis project is awesome and has helped me build projects, great work!\n\n## Contributors\n\n- [Marc Jeffrey](https://github.com/mjeffrey18) - creator and maintainer\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjeffrey18%2Ffast-jsonapi-serializer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmjeffrey18%2Ffast-jsonapi-serializer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjeffrey18%2Ffast-jsonapi-serializer/lists"}