Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Adding Custom Fields to Product, Category, or Customer

Adding Custom Fields to Product, Category, or Customer

~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Chapter 8 introduced the pattern using a product example. This chapter looks at three concrete, particularly common extension points in detail - including the respective pitfalls around $value.

Category: a custom banner field

app/code/Mironsoft/GraphqlDemo/etc/schema.graphqls
extend type CategoryInterface {
    mironsoft_banner_image: String
        @resolver(class: "Mironsoft\\GraphqlDemo\\Model\\Resolver\\Category\\BannerImage")
        @doc(description: "URL of a custom marketing banner shown above the category grid")
}

Unlike for products, the base type here is deliberately named CategoryInterface, not Category - Magento isn't always consistent in how it names its types. A quick look at introspection (chapter 3) or at vendor/magento/module-catalog-graph-ql/etc/schema.graphqls clears this up quickly, before extend type silently fails.

app/code/Mironsoft/GraphqlDemo/Model/Resolver/Category/BannerImage.php
<?php

declare(strict_types=1);

namespace Mironsoft\GraphqlDemo\Model\Resolver\Category;

use Magento\Catalog\Model\Category;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;

/**
 * Resolves the custom mironsoft_banner_image field on categories.
 */
class BannerImage implements ResolverInterface
{
    /**
     * Reads the mironsoft_banner_image attribute off the resolved category model.
     *
     * @param Field $field Resolved GraphQL field configuration
     * @param mixed $context Resolver context
     * @param ResolveInfo $info GraphQL resolve tree info
     * @param array|null $value Parent CategoryInterface resolver's value
     * @param array|null $args Arguments passed to this field
     * @return string|null
     */
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        ?array $value = null,
        ?array $args = null
    ): ?string {
        /** @var Category|null $category */
        $category = $value['model'] ?? null;

        $bannerImage = $category?->getData('mironsoft_banner_image');

        return is_string($bannerImage) && $bannerImage !== ''
            ? $bannerImage
            : null;
    }
}

Customer: reading a custom preference

CustomerOutput - the return type of the customer query - is a particularly sensitive extension point, since it practically always carries personal data. The resolver here doesn't automatically get its own customer ID handed to it - it has to read it explicitly from the context, just like the protected favorites mutation later in chapter 18:

app/code/Mironsoft/GraphqlDemo/etc/schema.graphqls
extend type CustomerOutput {
    mironsoft_newsletter_topic: String
        @resolver(class: "Mironsoft\\GraphqlDemo\\Model\\Resolver\\Customer\\NewsletterTopic")
        @doc(description: "The customer's preferred newsletter topic")
}

Here too, in practice the resolver usually ends up with a value from $value['model'] (the loaded \Magento\Customer\Model\Customer) - Magento's own CustomerOutput resolver already passes the loaded customer model along under this key.

When a new EAV attribute pays off vs. a GraphQL field

For products and categories, it's tempting to add every new field directly as its own EAV attribute via eav_attribute and make it editable through an admin form. That pays off when the value should genuinely be editorially maintained in the admin area (see the Admin Grids & Forms series). For purely computed values - such as mironsoft_badge_text from chapter 8, derived from existing data - a plain GraphQL resolver with no dedicated database attribute is entirely sufficient.

Achtung: extend type on a core type that internally relies on batch resolvers (e.g. price fields on products) can quickly become an N+1 bottleneck if your own field is naive and doesn't batch - a list of 50 products then triggers 50 separate extra database queries just for your custom field. Chapter 20 shows, using the events project, how to avoid this with batch resolvers - the same pattern also works for extended core types.

Chapter 10 wraps up block 3 and takes one more foundational look at each of the four parameters of resolve() - knowledge that's assumed for the custom events API starting in block 4.