custom fields and extension attributes
Magento GraphQL can be extended in a targeted way, but opening up the schema without preparation builds performance problems into your project through N+1 database queries. The right approach goes through extension attributes, cleanly isolated resolvers, and a clear separation between schema definition and data access.
Table of Contents
- 1. Why extending product data in Magento GraphQL is challenging
- 2. Extending the schema: ProductInterface and custom types
- 3. Implementing the resolver: clean and testable
- 4. Wiring up extension attributes correctly
- 5. Detecting and avoiding N+1 problems
- 6. Typical mistakes when extending the schema
- 7. EAV attributes in a GraphQL context
- 8. Wrong versus right, compared
- 9. Summary
- 10. At a glance
- 11. FAQ
1. Why extending product data in Magento GraphQL is challenging
Out of the box, Magento ships an extensive GraphQL schema for product data. ProductInterface covers most standard fields, but every project has specific requirements: a manufacturer label that comes from an EAV attribute, a delivery time from an external system, or a computed field that is only produced by business logic inside the resolver. These fields need to be integrated into the schema and into the resolver layer, without destabilizing the existing system.
The complexity comes from two directions: on one hand the schema must be defined correctly (types, nullability, descriptions), and on the other hand the resolver must deliver the data efficiently. Since a product list loads many products at once, a resolver that issues a separate database query per product runs straight into the classic N+1 problem. For 20 products, that means 21 database queries instead of one, measurably slower and exponentially worse as lists grow.
2. Extending the schema: ProductInterface and custom types
Schema extension in Magento GraphQL happens through .graphqls files in the etc/ directory of a module. The extend type keyword allows you to add new fields to an existing type without modifying the original type. For product fields, ProductInterface is the right anchor point: it is implemented by all product types (SimpleProduct, ConfigurableProduct, and so on) and ensures the new field is available on every product type.
Besides simple scalar fields (String, Int, Float, Boolean), you can also define custom types and use them as a field type. This is especially useful when the new field returns a structured piece of data, for example an object with several subfields. In that case, you first define your own type with type MyCustomData and then reference it inside extend type ProductInterface. The file must live in the module's etc/ directory and be named schema.graphqls.
# etc/schema.graphqls: extend ProductInterface with custom fields
extend type ProductInterface {
# Simple scalar field from EAV attribute
manufacturer_label: String @doc(description: "Human-readable manufacturer name from EAV attribute")
# Structured custom data as object type
delivery_info: DeliveryInfo @doc(description: "Delivery time and availability info from external system")
@resolver(class: "Vendor\\Module\\Model\\Resolver\\DeliveryInfo")
}
# Custom object type for structured return values
type DeliveryInfo {
estimated_days: Int @doc(description: "Estimated delivery days")
in_stock: Boolean! @doc(description: "Whether the product is currently in stock")
message: String @doc(description: "Optional human-readable delivery message")
}
3. Implementing the resolver: clean and testable
Every field with a @resolver annotation in the schema file needs a corresponding PHP class that implements ResolverInterface. The resolver receives access to the already-loaded product data through the $value parameter, the array returned by the parent product resolver. The product ID typically sits in $value['entity_id'] or $value['model']->getId().
What matters for resolver quality is a strict separation between presentation and data access. The resolver itself should not formulate SQL queries and should not know about services that do far more than what this single field needs. Instead, it delegates to a specialized service or repository that gets injected through constructor property promotion. That keeps the resolver small, readable, and testable through unit tests, without booting the Magento framework.
# Query to test the extended product fields
query ProductWithCustomFields {
products(search: "jacket", pageSize: 5) {
items {
sku
name
manufacturer_label
delivery_info {
estimated_days
in_stock
message
}
price_range {
minimum_price {
final_price { value currency }
}
}
}
}
}
4. Wiring up extension attributes correctly
Extension attributes are the official extension concept in Magento for service contracts. They allow you to attach extra data to models without changing the base class. In a GraphQL context, extension attributes are especially useful when the additional data is already made available while the product model is loading, for example through a plugin on the repository's load mechanism. That avoids separate database queries in the resolver, because the data is already sitting on the model.
There are two approaches to wiring extension attributes into the GraphQL resolver: either the resolver reads the attribute directly from the product model, when it is available in $value['model'], or it uses entity_id to load it through a service. The first approach is faster, the second more decoupled. Important: extension attributes are only loaded once they are explicitly requested, the Magento system does not load them automatically for every product. A plugin on the product repository load can change that and pre-populate the attribute.
5. Detecting and avoiding N+1 problems
The N+1 problem is ubiquitous in GraphQL and especially critical in Magento, because the database layer already generates plenty of queries through EAV. The classic scenario: a product list loads 20 products, and for each product the resolver fires an additional SELECT, for example to load the manufacturer label from the EAV table. That results in 20 extra queries for a single GraphQL request.
The correct solution is batching: instead of running one query per product, you collect all the IDs and run a single query for all products at once. In Magento, this can be implemented through a batch loader that gathers the IDs in a first phase and then loads all the data at once with a single repository query in a second phase. DataLoader libraries (well known from the JavaScript world) implement this pattern; in PHP, you can rebuild it with a simple accumulator pattern.
# BAD: This query triggers N+1 if each product loads manufacturer_label separately
# For 20 products = 1 products query + 20 manufacturer_label queries = 21 DB queries
query BadProductList {
products(search: "jacket", pageSize: 20) {
items {
sku
name
manufacturer_label # Resolver called once per product, N queries!
}
}
}
# GOOD: Same query, but resolver uses batch-loading strategy
# 1 products query + 1 batch query for all manufacturer labels = 2 DB queries
query GoodProductList {
products(search: "jacket", pageSize: 20) {
items {
sku
name
manufacturer_label # Resolver uses BatchLoader, single query for all IDs
}
}
}
6. Typical mistakes when extending the schema
A common mistake is writing SQL directly inside the resolver. That undermines the abstraction layer and makes the resolver impossible to test independently of the database structure. Repositories and service contracts are the correct approach, even though they require more code. Another mistake is loading the full product model inside the resolver when the ID and the required data are already available through the $value array. That leads to duplicate loads and unnecessary load on the system.
The schema definition itself frequently contains mistakes too: fields that always return a value should be declared as String! (non-nullable). Nullable fields (String without !) signal to the client that the value might be missing, which leads to optional handling on the frontend that is not actually necessary. Missing @doc annotations make the schema harder to self-document and are a frequent source of misunderstanding about field intent and values within teams.
7. EAV attributes in a GraphQL context
EAV attributes (Entity-Attribute-Value) are the standard way in Magento to extend product data without a schema migration. In a GraphQL context, though, loading EAV attribute values needs to be handled with care: EAV queries are internally expensive because the data is spread across multiple tables and requires joins. Loading an EAV attribute individually for every product quickly adds this overhead up to a critical amount.
The recommended strategy is to pre-load EAV attributes that are frequently used in a GraphQL context while loading the product, and attach them to the product model through extension attributes. Magento's model load mechanism does not automatically load all EAV attributes when using getProduct() or repository methods, they are loaded lazily. If you want to pre-load specific attributes on purpose, you can control that through the addAttributeToSelect method on the collection or through a plugin on the repository load.
8. Wrong versus right, compared
The following table summarizes the most common mistakes and their correct alternative when extending Magento GraphQL product data.
| Area | Wrong | Right | Reasoning |
|---|---|---|---|
| Data access in the resolver | Writing SQL directly in the resolver | Injecting a repository or service | Testable, decoupled, maintainable |
| Avoiding N+1 | Individual DB query per product | BatchLoader for all IDs at once | N queries becomes 1 query |
| Schema nullability | manufacturer_label: String (always has a value) |
manufacturer_label: String! |
Correct contract with the frontend |
| Loading EAV attributes | Individual getResource() load per product |
Pre-populate collection with attributes | Less JOIN overhead overall |
| Resolver size | Business logic directly in the resolver | Resolver delegates to a service | Resolver stays small and testable |
9. Summary
Extending product data in Magento GraphQL is well documented and cleanly achievable through schema extension with extend type. The real challenge is not the schema, it lies in the resolver implementation: data access must be batched to avoid N+1 problems, EAV attributes should be pre-loaded, and the resolver itself should contain no business logic and instead delegate to services. Extension attributes are the right tool for attaching additional data to the product model and making it efficiently accessible in the resolver.
The most important rule of thumb: every new field extension should be checked against the database queries it generates. Magento's query log or Xdebug profiling help make N+1 problems visible before they show up on the production system. Building extensions with a batching strategy from the start avoids costly retroactive refactoring that is hard to justify once the system is live.
Extending Product Data in Magento GraphQL: the essentials at a glance
Schema extension
extend type ProductInterface in etc/schema.graphqls, applies to all product types. Define nullability correctly, do not forget @doc annotations.
Resolver design
Implement ResolverInterface, delegate to a service, no SQL directly. Use constructor property promotion for DI. Keep the resolver under 30 lines.
Avoiding N+1
Use a BatchLoader for all product IDs at once. Pre-load EAV attributes through the collection. Reduce N separate queries down to a single one.
Extension attributes
The official Magento approach for model extensions. Pre-populate on repository load, query efficiently in the resolver. Keep the lazy-loading default in mind.