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
to_prepare: Spree’s models are not loaded yet when the
initializer file itself runs, so a bare Spree::Product there fails at boot
with uninitialized constant Spree::Product.
Standard resource endpoints append these to their own allowlist, so one
declaration covers the model’s create and update endpoints.
A product write also carries its media and variants as nested lists, and those
lists take what Spree::Media and Spree::Variant declare. One line makes a
media column writable both through the media endpoints and as part of a product
save:
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
Spree::Media and Spree::Variant.
Only declare attributes a seller may safely set on their own records.
Two endpoints deliberately ignore this hook because their parameters are
authorization data rather than resource data: API keys and invitations.
From an endpoint to agent tools and an SDK client
An endpoint you add reaches the Admin MCP and a typed SDK client through one path: document it, and generate the rest from the document.- Scaffold it.
spree generate api_resource Brand name:stringwrites the model, the controllers with theirrequest_params, the serializers, the routes, the permission and the specs, including an Admin integration spec that documents the endpoints. - Document it. The integration specs are rswag specs using Spree’s helpers:
admin_operationfor authentication and the permission,request_body_forfor the bodyrequest_paramsdeclares,filter_parameters_forfor list filters.spree task openapi:generate(bundle exec rake spree:openapi:generatein an extension) writes them intoopenapi/admin.yaml, andopenapi/store.yamlfor Store API endpoints. See documenting your endpoints. - Agent tools follow. Every documented admin action other than create, read, update and delete is an agent tool in the Admin MCP and the dashboard assistant, with nothing to register.
spec/agent_tools_spec.rbfails when an action is added undocumented. - Generate the client. In an npm package for the extension,
spree sdk generatewrites one SDK extension per resource, with a method per documented operation, and the request, filter and response types. An app adds them to its client withextendAdminClient(createAdminClient(…))orextendStoreClient(createClient(…)). See shipping a client for an extension.
client.request is enough; see the SDK tutorial.
