Customizing JSON Responses
Spree uses Alba serializers to build API v3 responses. You can replace any serializer via Spree Dependencies.Adding Custom Attributes
Let’s say you want to add amy_custom_attribute column to the Product API response.
Create a custom serializer that inherits from the core one:
app/serializers/my_app/product_serializer.rb
config/initializers/spree.rb:
Adding an Association
Let’s say you’ve created aSpree::Brand model that belongs to Product (see the tutorial for the full example).
Create a serializer for the new model:
app/serializers/spree/api/v3/brand_serializer.rb
app/serializers/my_app/product_serializer.rb
config/initializers/spree.rb:
?expand=brand:
Adding a Field from an Extension
Replacing a serializer works for your own application, but the setting holds one replacement. An extension that replacedadmin_product_serializer would be silently undone by an application that replaces it too. An extension adds read-only fields to a core serializer instead: an attribute, written as it is, or an association, a related record or a list of them written by the extension’s own serializer:
server/config/initializers/my_extension.rb
- The name is the response key and the record’s method that gives the value:
method:for an attribute,one:ormany:for an association.one: :warrantywritesproduct.warrantywith the given serializer;many: :warranty_claimswrites each ofproduct.warranty_claims. An extension adds such methods to core models with model extensions. - A simple computed value needs no method: name the field and pass
value:, as inattribute(:admin_product_serializer, :warranty_months, value: ->(product) { ... }). An association takesvalue:the same way, beside itsone:ormany:. - Registered by dependency key, such as
admin_product_serializer. If the application replaces that serializer with one that inherits from a core serializer, as shown above, the field still renders on the replacement. expand: trueshows the field only when the request asks for it with?expand=warranty. Without it, the field is always shown. An association’s nested expands reach its serializer, so?expand=warranty.claimspassesclaimson, the same way?expand=variants.mediaworks. Lists apply the same expands to every item.- Associations read on lists are batched when they are Active Record associations, as core ones are. A value computed by
value:is read for each record. - Names are unique per serializer. Registering a name that the serializer or another extension already uses raises an error at boot.
- Not in the generated SDK types. The extension that adds a field declares its TypeScript type.
Serializer Dependency Keys
Here are the most commonly customized v3 serializers:
See
Spree::Api::ApiDependencies for the full list.
Adding New Endpoints
Controller
Inherit fromSpree::Api::V3::Store::ResourceController for Store API endpoints:
app/controllers/spree/api/v3/store/brands_controller.rb
ResourceController provides:
Override these methods to customize:
Routes
Add routes in your app’sconfig/routes.rb:
Request Parameters
For endpoints that accept writes (create/update), declare what the body takes withrequest_params, at the top of the controller. Each attribute names its
type, the way a model attribute or a preference does:
app/controllers/spree/api/v3/admin/brands_controller.rb
ProductCreateParams, ProductUpdateParams, …) are generated from it, so the
controller, the reference and the TypeScript types cannot drift apart.
A custom action declares its own body with a name ending in
Params:
request_params 'BrandMergeParams', action: :merge do |body| ... end.
A shape several endpoints accept, such as an address or a price, is a class in
app/request_schemas, declared the way a model declares its attributes. A
subclass starts from its parent’s attributes:
app/request_schemas/spree/api/v3/brand_logo_request_schema.rb
body.attribute :logo, :object, shape: Spree::Api::V3::BrandLogoRequestSchema,
or take its attributes into their own body with body.include_attributes Spree::Api::V3::BrandLogoRequestSchema.
Without publishes the class is a set of attributes other declarations
include, not a type of its own.
An endpoint that needs logic of its own can still override
resource_permitted_attributes with a plain params.permit list, but then
its body is neither documented nor typed. Never override
permitted_attributes: it is what appends extension-contributed attributes
to your list, so overriding it replaces them.Adding an Attribute to a Core Resource
To make a new column writable on a Spree model without decorating the controllers that write it, append to the model’s permitted attributes:config/initializers/spree.rb
+=, never = — assigning replaces what another extension added — and
declare only attributes of your own, since redeclaring a key the controller
already permits replaces its filter rather than widening it.
For merchant-managed data, prefer Custom Fields,
which need no code and are filterable and sortable. This is for extensions
that add real database columns.
Writable by sellers
The Seller API accepts a narrower set of attributes than the Admin API, so the list above does not apply to it — an attribute you open to operators stays operator-only. To let sellers write it too, for example from a field on the seller panel’s product page, declare it again on the seller list:config/initializers/spree.rb

