├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── rake └── rubocop ├── json-serializer.gemspec ├── lib └── json_serializer.rb └── test ├── association_test.rb ├── attribute_test.rb ├── helper.rb └── root_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /.rubocop-* 3 | /Gemfile.lock 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: https://raw.githubusercontent.com/frodsan/dotfiles/master/.rubocop.yml 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | sudo: false 4 | rvm: 5 | - 2.2.5 6 | - 2.3.1 7 | script: 8 | - bin/rake test 9 | - bin/rubocop -D 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Francesco Rodríguez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | json-serializer [![Build Status](https://travis-ci.org/frodsan/json-serializer.svg)](https://travis-ci.org/frodsan/json-serializer) 2 | =============== 3 | 4 | Customizes JSON output through serializer objects. 5 | 6 | Installation 7 | ------------ 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem "json-serializer" 13 | ``` 14 | 15 | And then execute: 16 | 17 | ``` 18 | $ bundle 19 | ``` 20 | 21 | Or install it yourself as: 22 | 23 | ``` 24 | $ gem install json-serializer 25 | ``` 26 | 27 | Usage 28 | ----- 29 | 30 | Here's a simple example: 31 | 32 | ```ruby 33 | require "json_serializer" 34 | 35 | class UserSerializer < JsonSerializer 36 | attribute :id 37 | attribute :first_name 38 | attribute :last_name 39 | end 40 | ``` 41 | 42 | In this case, we defined a new serializer class and specified the attributes 43 | we would like to include in the serialized form. 44 | 45 | ```ruby 46 | user = User.create(first_name: "Sonny", last_name: "Moore", admin: true) 47 | 48 | UserSerializer.new(user).to_json 49 | # => "{\"id\":1,\"first_name\":\"Sonny\",\"last_name\":\"Moore\"}" 50 | ``` 51 | 52 | You can add a root to the outputted json through the `:root` option: 53 | 54 | ```ruby 55 | user = User.create(first_name: "Sonny", last_name: "Moore", admin: true) 56 | 57 | UserSerializer.new(user).to_json(root: :user) 58 | # => "{\"user\":{\"id\":1,\"first_name\":\"Sonny\",\"last_name\":\"Moore\"}}" 59 | ``` 60 | 61 | Arrays 62 | ------ 63 | 64 | A serializer can be used for objects contained in an array: 65 | 66 | ```ruby 67 | require "json_serializer" 68 | 69 | class PostSerializer < JsonSerializer 70 | attribute :id 71 | attribute :title 72 | attribute :body 73 | end 74 | 75 | posts = [] 76 | posts << Post.create(title: "Post 1", body: "Hello!") 77 | posts << Post.create(title: "Post 2", body: "Goodbye!") 78 | 79 | PostSerializer.new(posts).to_json 80 | ``` 81 | 82 | Given the example above, it will return a json output like: 83 | 84 | ```json 85 | [ 86 | { "id": 1, "title": "Post 1", "body": "Hello!" }, 87 | { "id": 2, "title": "Post 2", "body": "Goodbye!" } 88 | ] 89 | ``` 90 | 91 | Attributes 92 | ---------- 93 | 94 | By default, before looking up the attribute on the object, it checks the presence 95 | of a method with the name of the attribute. This allow serializes to include 96 | properties in addition to the object attributes or customize the result of a 97 | specified attribute. You can access the object being serialized with the +object+ 98 | method. 99 | 100 | ```ruby 101 | require "json_serializer" 102 | 103 | class UserSerializer < JsonSerializer 104 | attribute :id 105 | attribute :first_name 106 | attribute :last_name 107 | attribute :full_name 108 | 109 | def full_name 110 | object.first_name + " " + object.last_name 111 | end 112 | end 113 | 114 | user = User.create(first_name: "Sonny", last_name: "Moore") 115 | 116 | UserSerializer.new(user).to_json 117 | # => "{\"id\":1,\"first_name\":\"Sonny\",\"last_name\":\"Moore\",\"full_name\":\"Sonny Moore\"}" 118 | ``` 119 | 120 | If you would like direct, low-level control of attribute serialization, you can 121 | completely override `to_hash` method to return the hash you need: 122 | 123 | ```ruby 124 | require "json_serializer" 125 | 126 | class UserSerializer < JsonSerializer 127 | attribute :id 128 | attribute :first_name 129 | attribute :last_name 130 | 131 | attr_reader :current_user 132 | 133 | def initialize(object, current_user) 134 | super(object) 135 | @current_user = current_user 136 | end 137 | 138 | def to_hash 139 | hash = super 140 | hash.merge!(admin: object.admin) if current_user.admin? 141 | hash 142 | end 143 | end 144 | ``` 145 | 146 | Attributes with Custom Serializer 147 | --------------------------------- 148 | 149 | You can specify a serializer class for a defined attribute. This is very useful 150 | for serializing each element of an association. 151 | 152 | ```ruby 153 | require "json_serializer" 154 | 155 | class UserSerializer < JsonSerializer 156 | attribute :id 157 | attribute :username 158 | end 159 | 160 | class PostSerializer < JsonSerializer 161 | attribute :id 162 | attribute :title 163 | attribute :user, :UserSerializer 164 | attribute :comments, :CommentSerializer 165 | end 166 | 167 | class CommentSerializer < JsonSerializer 168 | attribute :id 169 | attribute :content 170 | attribute :user, :UserSerializer 171 | end 172 | 173 | admin = User.create(username: "admin", admin: true) 174 | user = User.create(username: "user") 175 | 176 | post = Post.create(title: "Hello!", user: admin) 177 | post.comments << Comment.create(content: "First comment", user: user) 178 | 179 | PostSerializer.new(post).to_json 180 | ``` 181 | 182 | The example above returns the following json output: 183 | 184 | ```json 185 | { 186 | "id": 1, 187 | "title": "Hello!", 188 | "user": 189 | { 190 | "id": 1, 191 | "username": "admin" 192 | }, 193 | "comments": 194 | [ 195 | { 196 | "id": 1, 197 | "content": "First comment", 198 | "user": 199 | { 200 | "id": 2, 201 | "username": "user" 202 | } 203 | } 204 | ] 205 | } 206 | ``` 207 | 208 | Contributing 209 | ------------ 210 | 211 | Fork the project with: 212 | 213 | ``` 214 | $ git clone git@github.com:frodsan/json-serializer.git 215 | ``` 216 | 217 | To install dependencies, use: 218 | 219 | ``` 220 | $ bundle install 221 | ``` 222 | 223 | To run the test suite, do: 224 | 225 | ``` 226 | $ rake test 227 | ``` 228 | 229 | For bug reports and pull requests use [GitHub][issues]. 230 | 231 | License 232 | ------- 233 | 234 | This gem is released under the [MIT License][mit]. 235 | 236 | [active_model_serializers]: https://github.com/rails-api/active_model_serializers 237 | [mit]: http://www.opensource.org/licenses/MIT 238 | [issues]: https://github.com/frodsan/json-serializer/issues 239 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake/testtask" 4 | 5 | Rake::TestTask.new do |t| 6 | t.pattern = "test/*_test.rb" 7 | t.warning = true 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rake' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require "pathname" 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require "rubygems" 14 | require "bundler/setup" 15 | 16 | load Gem.bin_path("rake", "rake") 17 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rubocop' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require "pathname" 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require "rubygems" 14 | require "bundler/setup" 15 | 16 | load Gem.bin_path("rubocop", "rubocop") 17 | -------------------------------------------------------------------------------- /json-serializer.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "json-serializer" 3 | s.version = "1.0.0" 4 | s.summary = "Customize JSON ouput through serializer objects" 5 | s.description = s.summary 6 | s.author = "Francesco Rodríguez" 7 | s.email = "hello@frodsan.com" 8 | s.homepage = "https://github.com/frodsan/json-serializer" 9 | s.license = "MIT" 10 | 11 | s.files = Dir["LICENSE", "README.md", "lib/**/*.rb"] 12 | s.test_files = Dir["test/**/*.rb"] 13 | 14 | s.add_development_dependency "minitest", "~> 5.8" 15 | s.add_development_dependency "minitest-sugar", "~> 2.1" 16 | s.add_development_dependency "rake", "~> 11.0" 17 | s.add_development_dependency "rubocop", "~> 0.39" 18 | end 19 | -------------------------------------------------------------------------------- /lib/json_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | 5 | class JsonSerializer 6 | module Utils 7 | def self.const(context, name) 8 | case name 9 | when Symbol, String 10 | context.const_get(name) 11 | else name 12 | end 13 | end 14 | end 15 | 16 | def self.inherited(subclass) 17 | attributes.each do |name, serializer| 18 | subclass.attribute(name, serializer) 19 | end 20 | end 21 | 22 | def self.attribute(name, serializer = nil) 23 | attributes[name] ||= serializer 24 | end 25 | 26 | def self.attributes 27 | @attributes ||= {} 28 | end 29 | 30 | attr_reader :object 31 | 32 | def initialize(object) 33 | @object = object 34 | end 35 | 36 | def to_json(root: nil) 37 | result = decorate 38 | result = { root => result } if root 39 | 40 | result.to_json 41 | end 42 | 43 | def decorate 44 | return nil unless object 45 | 46 | if object.respond_to?(:to_a) 47 | to_arry 48 | else 49 | to_hash 50 | end 51 | end 52 | 53 | protected 54 | 55 | def to_arry 56 | object.to_a.map { |o| self.class.new(o).to_hash } 57 | end 58 | 59 | def to_hash 60 | self.class.attributes.each_with_object({}) do |(name, serializer), hash| 61 | res = self.class.method_defined?(name) ? send(name) : object.send(name) 62 | res = Utils.const(self.class, serializer).new(res).decorate if serializer 63 | 64 | hash[name] = res 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/association_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | class OrganizationSerializer < JsonSerializer 6 | attribute :id 7 | attribute :name 8 | end 9 | 10 | class UserWithOrganizationSerializer < JsonSerializer 11 | attribute :id 12 | attribute :name 13 | attribute :organization, :OrganizationSerializer 14 | end 15 | 16 | class UserWithCustomOrganizationSerializer < JsonSerializer 17 | attribute :organizations, :OrganizationSerializer 18 | 19 | def organizations 20 | [Organization.new(id: 1, name: "enterprise")] 21 | end 22 | end 23 | 24 | # rubocop:disable ClassLength 25 | class AssociationTest < Minitest::Test 26 | test "serializes object with association" do 27 | user = User.new(id: 1, name: "sonny") 28 | user.organization = Organization.new(id: 1, name: "enterprise") 29 | 30 | result = { 31 | id: 1, 32 | name: "sonny", 33 | organization: { 34 | id: 1, 35 | name: "enterprise" 36 | } 37 | }.to_json 38 | 39 | assert_equal result, UserWithOrganizationSerializer.new(user).to_json 40 | end 41 | 42 | test "serializes object with a nil association" do 43 | user = User.new(id: 1, name: "sonny") 44 | user.organization = nil 45 | 46 | result = { 47 | id: 1, 48 | name: "sonny", 49 | organization: nil 50 | }.to_json 51 | 52 | assert_equal result, UserWithOrganizationSerializer.new(user).to_json 53 | end 54 | 55 | test "serializes array with association" do 56 | users = [ 57 | User.new( 58 | id: 1, 59 | name: "sonny", 60 | organization: Organization.new(id: 1, name: "enterprise") 61 | ), 62 | User.new( 63 | id: 2, 64 | name: "anton", 65 | organization: Organization.new(id: 2, name: "evil") 66 | ) 67 | ] 68 | 69 | result = [ 70 | { 71 | id: 1, 72 | name: "sonny", 73 | organization: { 74 | id: 1, 75 | name: "enterprise" 76 | } 77 | }, 78 | { 79 | id: 2, 80 | name: "anton", 81 | organization: { 82 | id: 2, 83 | name: "evil" 84 | } 85 | } 86 | ].to_json 87 | 88 | assert_equal result, UserWithOrganizationSerializer.new(users).to_json 89 | end 90 | 91 | class UserWithOrganizationsSerializer < JsonSerializer 92 | attribute :id 93 | attribute :name 94 | attribute :organizations, :OrganizationSerializer 95 | end 96 | 97 | test "serializes object with collection" do 98 | user = User.new(id: 1, name: "sonny") 99 | user.organizations = [ 100 | Organization.new(id: 1, name: "enterprise"), 101 | Organization.new(id: 2, name: "evil") 102 | ] 103 | 104 | result = { 105 | id: 1, 106 | name: "sonny", 107 | organizations: [ 108 | { 109 | id: 1, 110 | name: "enterprise" 111 | }, 112 | { 113 | id: 2, 114 | name: "evil" 115 | } 116 | ] 117 | }.to_json 118 | 119 | assert_equal result, UserWithOrganizationsSerializer.new(user).to_json 120 | end 121 | 122 | test "serializes array with nested collections" do 123 | users = [ 124 | User.new( 125 | id: 1, 126 | name: "sonny", 127 | organizations: [ 128 | Organization.new(id: 1, name: "enterprise"), 129 | Organization.new(id: 2, name: "evil") 130 | ] 131 | ), 132 | User.new( 133 | id: 2, 134 | name: "anton", 135 | organizations: [ 136 | Organization.new(id: 3, name: "showtek") 137 | ] 138 | ) 139 | ] 140 | 141 | result = [ 142 | { 143 | id: 1, 144 | name: "sonny", 145 | organizations: [ 146 | { 147 | id: 1, 148 | name: "enterprise" 149 | }, 150 | { 151 | id: 2, 152 | name: "evil" 153 | } 154 | ] 155 | }, 156 | { 157 | id: 2, 158 | name: "anton", 159 | organizations: [ 160 | { 161 | id: 3, 162 | name: "showtek" 163 | } 164 | ] 165 | } 166 | ].to_json 167 | 168 | assert_equal result, UserWithOrganizationsSerializer.new(users).to_json 169 | end 170 | 171 | test "implements association method and returns different result" do 172 | user = User.new 173 | 174 | result = { organizations: [{ id: 1, name: "enterprise" }] }.to_json 175 | 176 | assert_equal result, UserWithCustomOrganizationSerializer.new(user).to_json 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /test/attribute_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | class PostSerializer < JsonSerializer 6 | attribute :id 7 | attribute :title 8 | attribute :slug 9 | 10 | def slug 11 | "#{ object.id }-#{ object.title }" 12 | end 13 | end 14 | 15 | class ParentSerializer < JsonSerializer 16 | attribute :parent 17 | end 18 | 19 | class ChildSerializer < ParentSerializer 20 | attribute :child 21 | end 22 | 23 | class UserSerializer < JsonSerializer 24 | attribute :id 25 | attribute :fullname 26 | 27 | def fullname 28 | object.name + " " + object.lastname 29 | end 30 | end 31 | 32 | class AttributeTest < Minitest::Test 33 | test "converts defined attributes into json" do 34 | post = Post.new(id: 1, title: "tsunami") 35 | 36 | result = { 37 | id: 1, 38 | title: "tsunami", 39 | slug: "1-tsunami" 40 | }.to_json 41 | 42 | assert_equal result, PostSerializer.new(post).to_json 43 | end 44 | 45 | test "serializes array" do 46 | users = [ 47 | User.new(id: 1, name: "sonny", lastname: "moore"), 48 | User.new(id: 2, name: "anton", lastname: "zaslavski") 49 | ] 50 | 51 | result = [ 52 | { id: 1, fullname: "sonny moore" }, 53 | { id: 2, fullname: "anton zaslavski" } 54 | ].to_json 55 | 56 | assert_equal result, UserSerializer.new(users).to_json 57 | end 58 | 59 | test "inheritance" do 60 | parent = ParentSerializer.attributes 61 | child = ChildSerializer.attributes 62 | 63 | assert_equal(parent.merge(child: nil), child) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "minitest/autorun" 5 | require "minitest/pride" 6 | require "minitest/sugar" 7 | require "ostruct" 8 | require_relative "../lib/json_serializer" 9 | 10 | User = OpenStruct 11 | Organization = OpenStruct 12 | Person = OpenStruct 13 | Post = OpenStruct 14 | -------------------------------------------------------------------------------- /test/root_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | class PersonSerializer < JsonSerializer 6 | attribute :name 7 | end 8 | 9 | class RootTest < Minitest::Test 10 | setup do 11 | @person = Person.new(name: "sonny") 12 | end 13 | 14 | test "serialized object includes root" do 15 | result = { person: @person.to_h }.to_json 16 | 17 | assert_equal result, PersonSerializer.new(@person).to_json(root: :person) 18 | end 19 | 20 | test "serialized array includes root" do 21 | result = { people: [@person.to_h] }.to_json 22 | 23 | assert_equal result, PersonSerializer.new([@person]).to_json(root: :people) 24 | end 25 | end 26 | --------------------------------------------------------------------------------