When working with ActiveModel::Serialization in a Ruby on Rails API, it’s common to organize serializers using inheritance—especially when building and maintaining different versions of your API.
One potential pitfall: Rails includes all attributes declared in the parent serializer by default, even when rendering from a child. This can lead to unexpected fields appearing in API responses for earlier versions.
Here’s how to address that.
The Context
Suppose a new serializer is created under API::V40
, intended to include everything already exposed by API::V36::ProgramGuide
. To avoid duplication, shared logic is moved to the V40 serializer, and the V36 version inherits from it.
The only difference is that V40 needs to expose an additional field — about_host
— which must not appear in V36 responses.
The Problem
The V40 serializer might look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# app/serializers/api/v40/accommodation_serializer.rb
module Api
module V40
class AccommodationSerializer < ActiveModel::Serializer
attributes :main_description, :images_gallery, :about_host
def main_description
object.description
end
def images_gallery
object.photos.map do |photo|
{
url: photo.file.url(:large),
caption: photo.caption
}
end
end
def about_host
[
{
title: I18n.t(".accommodations.what_offers"),
description: object.what_offers_description
}
]
end
end
end
end
The V36 serializer inherits from V40 and adds two more attributes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/serializers/api/v36/program_guide/accommodation_serializer.rb
module Api
module V36
module ProgramGuide
class AccommodationSerializer < Api::V40::AccommodationSerializer
attributes :extra_information, :faq
def extra_information
object.accommodation_info_sections.sections.map do |info_section|
ExtraInformationPreviewSerializer.new(info_section, root: false)
end
end
def faq
object.accommodation_info_sections.faq.map do |info_section|
FaqPreviewSerializer.new(info_section, root: false)
end
end
end
end
end
end
Even though only :extra_information
and :faq
are declared in the V36 serializer, Rails includes :main_description
, :images_gallery
, and :about_host
in the JSON — since they’re inherited from the parent.
This creates a problem when a field like about_host
is version-specific.
The Solution
To prevent specific attributes from being included in earlier versions, override the attributes
method in the child serializer and explicitly exclude the undesired field:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# app/serializers/api/v36/program_guide/accommodation_serializer.rb
module Api
module V36
module ProgramGuide
class AccommodationSerializer < Api::V40::AccommodationSerializer
attributes :extra_information, :faq
def extra_information
object.accommodation_info_sections.sections.map do |info_section|
ExtraInformationPreviewSerializer.new(info_section, root: false)
end
end
def faq
object.accommodation_info_sections.faq.map do |info_section|
FaqPreviewSerializer.new(info_section, root: false)
end
end
private
def attributes(*args)
super.except(:about_host)
end
end
end
end
end
Why This Works
The attributes(*args)
method returns a hash of all key-value pairs that will be rendered in the response. By chaining .except(:about_host)
, only that specific field is removed, while all other inherited logic remains intact.
When to Use This
Use this approach when:
- Managing versioned APIs where newer versions introduce additional fields
- Sharing logic across serializers while limiting exposure of specific fields
- Refactoring serializers to reduce duplication without introducing breaking changes
- Maintaining precise control over response formats in legacy endpoints
When Not to Use It
If serializers for different versions start to diverge significantly, inheritance may add more complexity than it removes. In such cases, defining serializers independently might offer better clarity and maintainability.