<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sergiynezbritskiy.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sergiynezbritskiy.github.io/" rel="alternate" type="text/html" /><updated>2026-05-11T10:51:35+00:00</updated><id>https://sergiynezbritskiy.github.io/feed.xml</id><title type="html">Sergiy Nezbritskiy Personal Blog</title><subtitle>Just casual posts about engineering.</subtitle><author><name>Sergiy Nezbritskiy</name></author><entry><title type="html">Magento 2: Adding a Custom Shipping Address Field Tutorial (Part 1 - Defining the Attribute)</title><link href="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-1.html" rel="alternate" type="text/html" title="Magento 2: Adding a Custom Shipping Address Field Tutorial (Part 1 - Defining the Attribute)" /><published>2025-10-10T00:00:00+00:00</published><updated>2025-10-10T00:00:00+00:00</updated><id>https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-1</id><content type="html" xml:base="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-1.html"><![CDATA[<p>At first glance, adding a custom attribute to a customer address in Magento 2 seems like it should be straightforward. In reality, it’s anything but. Even a task that sounds trivial quickly becomes complex: you need to write a lot of boilerplate code, deal with quirks and bugs, and carefully specify each step - often with no clear guidance on where or how to implement it. Debugging is almost inevitable, and it can feel like every action requires painstaking attention.</p>

<p>This article guides you through the full process of implementing a custom customer address attribute in Magento 2. Divided into three parts, it covers everything from defining the attribute and adding options to making it visible and functional across checkout, order details, and the customer account dashboard.</p>

<p>Of course, if you’d rather skip the long setup, there’s a shortcut: you can download the ready-made module <a href="https://github.com/sergiynezbritskiy/module-custom-address-field">here</a>, rename the <code class="language-plaintext highlighter-rouge">custom_field</code> attribute to whatever you need, maybe add some adjustments, and it’s ready to go. But for those who want to understand the “why” and “how”, this guide walks through every necessary step.</p>

<h3 id="prerequisites">Prerequisites</h3>

<p>For this tutorial, we’ll need a vanilla Magento 2 installation with sample data. All filenames are relative to <code class="language-plaintext highlighter-rouge">app/code/SergiyNezbritskiy/CustomShippingAddress</code>.</p>

<h3 id="task">Task</h3>

<p>A custom address attribute must be added to the customer address entity. The attribute should be a required dropdown field populated with a predefined list of options. It must be displayed consistently across all relevant areas of the storefront and admin interface, including checkout, order details, and the customer account dashboard.</p>

<h3 id="step-1-create-a-module">Step 1. Create a Module</h3>

<p>Create a module named <code class="language-plaintext highlighter-rouge">SergiyNezbritskiy_CustomAddressField</code>.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">registration.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">use</span> <span class="nc">Magento\Framework\Component\ComponentRegistrar</span><span class="p">;</span>

<span class="nc">ComponentRegistrar</span><span class="o">::</span><span class="nf">register</span><span class="p">(</span><span class="nc">ComponentRegistrar</span><span class="o">::</span><span class="no">MODULE</span><span class="p">,</span> <span class="s1">'SergiyNezbritskiy_CustomAddressField'</span><span class="p">,</span> <span class="k">__DIR__</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/module.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Module/etc/module.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"SergiyNezbritskiy_CustomAddressField"</span> <span class="na">setup_version=</span><span class="s">"1.0.0"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>Now enable the module:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
</code></pre></div></div>

<h4 id="verification">Verification</h4>

<p>You should see the module listed and enabled in <code class="language-plaintext highlighter-rouge">app/etc/config.php</code>, for example:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>
<span class="k">return</span> <span class="p">[</span>
    <span class="s1">'modules'</span> <span class="o">=&gt;</span> <span class="p">[</span>
        <span class="c1">// Other modules in your project</span>
        <span class="s1">'SergiyNezbritskiy_CustomAddressField'</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
        <span class="c1">// Other modules in your project</span>
    <span class="p">]</span>
<span class="p">];</span>
</code></pre></div></div>

<h3 id="step-2-create-the-shipping-address-attribute">Step 2. Create the Shipping Address Attribute</h3>

<p>Add a module dependency for <code class="language-plaintext highlighter-rouge">Magento_Customer</code> in <code class="language-plaintext highlighter-rouge">module.xml</code>.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/module.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version = "1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Module/etc/module.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"SergiyNezbritskiy_CustomAddressField"</span> <span class="na">setup_version=</span><span class="s">"1.0.0"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;sequence&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Customer"</span><span class="nt">/&gt;</span>
        <span class="nt">&lt;/sequence&gt;</span>
    <span class="nt">&lt;/module&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>Create a source model for the attribute options.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Model/Address/Attribute/Source/CustomField.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Model\Address\Attribute\Source</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Eav\Model\Entity\Attribute\Source\AbstractSource</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">CustomField</span> <span class="kd">extends</span> <span class="nc">AbstractSource</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">getAllOptions</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next, create a data patch to define the attribute.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Setup/Patch/Data/CreateShippingAttribute.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Setup\Patch\Data</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Customer\Model\Indexer\Address\AttributeProvider</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Customer\Setup\CustomerSetup</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Eav\Model\AttributeRepository</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\Setup\ModuleDataSetupInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\Setup\Patch\DataPatchInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\Setup\Patch\PatchRevertableInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">SergiyNezbritskiy\CustomAddressField\Model\Address\Attribute\Source\CustomField</span><span class="p">;</span>

<span class="k">readonly</span> <span class="kd">class</span> <span class="nc">CreateShippingAttribute</span> <span class="kd">implements</span> <span class="nc">DataPatchInterface</span><span class="p">,</span> <span class="nc">PatchRevertableInterface</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="no">string</span> <span class="no">ATTRIBUTE_CODE</span> <span class="o">=</span> <span class="s1">'custom_field'</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span>
        <span class="k">private</span> <span class="kt">ModuleDataSetupInterface</span> <span class="nv">$moduleDataSetup</span><span class="p">,</span>
        <span class="k">private</span> <span class="kt">CustomerSetup</span>            <span class="nv">$customerSetup</span><span class="p">,</span>
        <span class="k">private</span> <span class="kt">AttributeRepository</span>      <span class="nv">$attributeRepository</span>
    <span class="p">)</span> <span class="p">{}</span>

    <span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="n">getDependencies</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[];</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">getAliases</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[];</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">apply</span><span class="p">():</span> <span class="kt">DataPatchInterface</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">moduleDataSetup</span><span class="o">-&gt;</span><span class="nf">getConnection</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">startSetup</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">saveAttribute</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">moduleDataSetup</span><span class="o">-&gt;</span><span class="nf">getConnection</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">endSetup</span><span class="p">();</span>

        <span class="k">return</span> <span class="nv">$this</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">function</span> <span class="n">saveAttribute</span><span class="p">():</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$definition</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'type'</span> <span class="o">=&gt;</span> <span class="s1">'varchar'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="s1">'Custom Field'</span><span class="p">,</span>
            <span class="s1">'input'</span> <span class="o">=&gt;</span> <span class="s1">'select'</span><span class="p">,</span>
            <span class="s1">'required'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'system'</span> <span class="o">=&gt;</span> <span class="kc">false</span><span class="p">,</span>
            <span class="s1">'sort_order'</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
            <span class="s1">'position'</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
            <span class="s1">'is_used_in_grid'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'is_visible_in_grid'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'is_filterable_in_grid'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'is_searchable_in_grid'</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span>
            <span class="s1">'source'</span> <span class="o">=&gt;</span> <span class="nc">CustomField</span><span class="o">::</span><span class="n">class</span><span class="p">,</span>
            <span class="s1">'global'</span> <span class="o">=&gt;</span> <span class="nc">ScopedAttributeInterface</span><span class="o">::</span><span class="no">SCOPE_GLOBAL</span><span class="p">,</span>
        <span class="p">];</span>

        <span class="nv">$connection</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">moduleDataSetup</span><span class="o">-&gt;</span><span class="nf">getConnection</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">customerSetup</span><span class="o">-&gt;</span><span class="nf">addAttribute</span><span class="p">(</span><span class="nc">AttributeProvider</span><span class="o">::</span><span class="no">ENTITY</span><span class="p">,</span> <span class="k">self</span><span class="o">::</span><span class="no">ATTRIBUTE_CODE</span><span class="p">,</span> <span class="nv">$definition</span><span class="p">);</span>

        <span class="nv">$attribute</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">customerSetup</span><span class="o">-&gt;</span><span class="nf">getEavConfig</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">getAttribute</span><span class="p">(</span><span class="nc">AttributeProvider</span><span class="o">::</span><span class="no">ENTITY</span><span class="p">,</span> <span class="k">self</span><span class="o">::</span><span class="no">ATTRIBUTE_CODE</span><span class="p">);</span>

        <span class="nv">$customerFormsTable</span> <span class="o">=</span> <span class="nv">$connection</span><span class="o">-&gt;</span><span class="nf">getTableName</span><span class="p">(</span><span class="s1">'customer_form_attribute'</span><span class="p">);</span>
        <span class="nv">$connection</span><span class="o">-&gt;</span><span class="nf">insertMultiple</span><span class="p">(</span><span class="nv">$customerFormsTable</span><span class="p">,</span> <span class="p">[</span>
            <span class="p">[</span><span class="s1">'form_code'</span> <span class="o">=&gt;</span> <span class="s1">'adminhtml_customer_address'</span><span class="p">,</span> <span class="s1">'attribute_id'</span> <span class="o">=&gt;</span> <span class="nv">$attribute</span><span class="o">-&gt;</span><span class="nf">getAttributeId</span><span class="p">()],</span>
            <span class="p">[</span><span class="s1">'form_code'</span> <span class="o">=&gt;</span> <span class="s1">'customer_address_edit'</span><span class="p">,</span> <span class="s1">'attribute_id'</span> <span class="o">=&gt;</span> <span class="nv">$attribute</span><span class="o">-&gt;</span><span class="nf">getAttributeId</span><span class="p">()],</span>
            <span class="p">[</span><span class="s1">'form_code'</span> <span class="o">=&gt;</span> <span class="s1">'customer_register_address'</span><span class="p">,</span> <span class="s1">'attribute_id'</span> <span class="o">=&gt;</span> <span class="nv">$attribute</span><span class="o">-&gt;</span><span class="nf">getAttributeId</span><span class="p">()],</span>
        <span class="p">]);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">revert</span><span class="p">():</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$attribute</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">customerSetup</span><span class="o">-&gt;</span><span class="nf">getEavConfig</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">getAttribute</span><span class="p">(</span><span class="nc">AttributeProvider</span><span class="o">::</span><span class="no">ENTITY</span><span class="p">,</span> <span class="k">self</span><span class="o">::</span><span class="no">ATTRIBUTE_CODE</span><span class="p">);</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">attributeRepository</span><span class="o">-&gt;</span><span class="nb">delete</span><span class="p">(</span><span class="nv">$attribute</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Apply the patch:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
</code></pre></div></div>

<p>After this, you should see the new attribute in the Admin Dashboard on the Create/Edit Address form:</p>

<p><img src="/assets/images/custom-address-field/01-admin-dashboard-create-address-no-options.png" alt="" title="Create Address Form Without Options" /></p>

<p>Now let’s add some options to the attribute source file.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Model/Address/Attribute/Source/CustomField.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Model\Address\Attribute\Source</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Eav\Model\Entity\Attribute\Source\AbstractSource</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">CustomField</span> <span class="kd">extends</span> <span class="nc">AbstractSource</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">getAllOptions</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="nv">$result</span> <span class="o">=</span> <span class="p">[];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">''</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Select Custom Field'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-1'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 1'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-2'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 2'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-3'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 3'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-4'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 4'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-5'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 5'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="nv">$result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="s1">'option-6'</span><span class="p">,</span>
            <span class="s1">'label'</span> <span class="o">=&gt;</span> <span class="nf">__</span><span class="p">(</span><span class="s1">'Option 6'</span><span class="p">)</span>
        <span class="p">];</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="verification-1">Verification</h4>

<p>Now the field should be fully functioning in the admin dashboard:
<img src="/assets/images/custom-address-field/02-admin-dashboard-create-address-with-options.png" alt="" title="Create Address Form With Options" /></p>

<p>The field should be saved into <code class="language-plaintext highlighter-rouge">customer_address_entity_varchar</code> table. You can check it by running next SQL query:</p>

<pre><code class="language-mysql">select *
from customer_address_entity_varchar
where attribute_id = (select attribute_id
                      from eav_attribute
                      where 'custom_field' = attribute_code)
</code></pre>

<p>This is what you expect to see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+--------------+-----------+----------+
| value_id | attribute_id | entity_id | value    |
+----------+--------------+-----------+----------+
|        1 |          157 |         1 | option-4 |
+----------+--------------+-----------+----------+
</code></pre></div></div>

<h3 id="step-3-make-the-customer-address-field-visible-in-the-address">Step 3. Make the customer address field visible in the address</h3>

<p>The address template can be configured in Admin Dashboard: <code class="language-plaintext highlighter-rouge">Stores</code> -&gt; <code class="language-plaintext highlighter-rouge">Settings</code> -&gt; <code class="language-plaintext highlighter-rouge">Configuration</code> -&gt; <code class="language-plaintext highlighter-rouge">Customers</code> -&gt; <code class="language-plaintext highlighter-rouge">Customer Configuration</code> -&gt; <code class="language-plaintext highlighter-rouge">Address Templates</code>. You can display this data the way you need it. In out case we just append it into the end of the address block, using a patch.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Setup/Patch/Data/UpdateAddressTemplates.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Setup\Patch\Data</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Framework\Setup\ModuleDataSetupInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\Setup\Patch\DataPatchInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\App\Config\Storage\WriterInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Framework\App\Config\ScopeConfigInterface</span><span class="p">;</span>

<span class="k">readonly</span> <span class="kd">class</span> <span class="nc">UpdateAddressTemplates</span> <span class="kd">implements</span> <span class="nc">DataPatchInterface</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span>
        <span class="k">private</span> <span class="kt">ModuleDataSetupInterface</span> <span class="nv">$moduleDataSetup</span><span class="p">,</span>
        <span class="k">private</span> <span class="kt">WriterInterface</span>          <span class="nv">$configWriter</span><span class="p">,</span>
        <span class="k">private</span> <span class="kt">ScopeConfigInterface</span>     <span class="nv">$scopeConfig</span>
    <span class="p">)</span>
    <span class="p">{</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">apply</span><span class="p">():</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">moduleDataSetup</span><span class="o">-&gt;</span><span class="nf">getConnection</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">startSetup</span><span class="p">();</span>

        <span class="c1">// Define config paths and their appended strings</span>
        <span class="nv">$map</span> <span class="o">=</span> <span class="p">[</span>
            <span class="s1">'customer/address_templates/text'</span> <span class="o">=&gt;</span> <span class="s1">''</span> <span class="mf">.</span> <span class="kc">PHP_EOL</span> <span class="mf">.</span> <span class="s1">'Custom Field: '</span><span class="p">,</span>
            <span class="s1">'customer/address_templates/oneline'</span> <span class="o">=&gt;</span> <span class="s1">', '</span><span class="p">,</span>
            <span class="s1">'customer/address_templates/html'</span> <span class="o">=&gt;</span> <span class="s1">' &lt;br /&gt;'</span><span class="p">,</span>
            <span class="s1">'customer/address_templates/pdf'</span> <span class="o">=&gt;</span> <span class="s1">' '</span><span class="mf">.</span><span class="kc">PHP_EOL</span><span class="mf">.</span><span class="s1">' '</span><span class="p">,</span>
        <span class="p">];</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$map</span> <span class="k">as</span> <span class="nv">$path</span> <span class="o">=&gt;</span> <span class="nv">$appendString</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$currentValue</span> <span class="o">=</span> <span class="p">(</span><span class="n">string</span><span class="p">)</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">scopeConfig</span><span class="o">-&gt;</span><span class="nf">getValue</span><span class="p">(</span><span class="nv">$path</span><span class="p">);</span>
            <span class="nv">$newValue</span> <span class="o">=</span> <span class="nb">rtrim</span><span class="p">(</span><span class="nv">$currentValue</span><span class="p">)</span> <span class="mf">.</span> <span class="nv">$appendString</span><span class="p">;</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">configWriter</span><span class="o">-&gt;</span><span class="nf">save</span><span class="p">(</span><span class="nv">$path</span><span class="p">,</span> <span class="nv">$newValue</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">moduleDataSetup</span><span class="o">-&gt;</span><span class="nf">getConnection</span><span class="p">()</span><span class="o">-&gt;</span><span class="nf">endSetup</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="n">getDependencies</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[];</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">getAliases</span><span class="p">():</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[];</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Apply the patch</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
php bin/magento cache:clean config
</code></pre></div></div>

<h4 id="verification-2">Verification:</h4>

<p>When login to customer account you should see your field in the shipping address</p>

<p><img src="/assets/images/custom-address-field/03-customer-account-address-display-custom-field.png" alt="" title="Customer Account Addresses with Custom Field" /></p>

<p><strong>Next up:</strong> in part 2, we’ll cover how to expose this field in checkout forms, customer account pages, and REST APIs.</p>]]></content><author><name>Sergiy Nezbritskiy</name></author><category term="Magento 2" /><category term="Checkout" /><category term="Development" /><summary type="html"><![CDATA[At first glance, adding a custom attribute to a customer address in Magento 2 seems like it should be straightforward. In reality, it’s anything but. Even a task that sounds trivial quickly becomes complex: you need to write a lot of boilerplate code, deal with quirks and bugs, and carefully specify each step - often with no clear guidance on where or how to implement it. Debugging is almost inevitable, and it can feel like every action requires painstaking attention.]]></summary></entry><entry><title type="html">Magento 2: Adding Custom Shipping Address Field Tutorial (Part 2 - Checkout)</title><link href="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-2.html" rel="alternate" type="text/html" title="Magento 2: Adding Custom Shipping Address Field Tutorial (Part 2 - Checkout)" /><published>2025-10-10T00:00:00+00:00</published><updated>2025-10-10T00:00:00+00:00</updated><id>https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-2</id><content type="html" xml:base="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-2.html"><![CDATA[<h3 id="challenges">Challenges</h3>

<p>The process of saving custom shipping address attributes is one of the most complex aspects of Magento architecture due to system complexity, known bugs, and the substantial amount of boilerplate code required.</p>

<p>It is crucial to understand how Magento expects data to flow from customer interaction to the database. Here are the key points:</p>

<ol>
  <li>Magento’s frontend and backend communicate via REST API following interfaces defined within Magento.</li>
  <li>We cannot extend the customer address interface by simply adding a new field such as <code class="language-plaintext highlighter-rouge">custom_field</code>. Magento requires us to use extension attributes for this purpose.</li>
  <li>The Magento frontend does not operate with extension attributes by default. Instead, all custom address attributes are treated as the <code class="language-plaintext highlighter-rouge">address.custom_attributes</code> property.</li>
  <li>By default, the attributes we create do not automatically populate the <code class="language-plaintext highlighter-rouge">address.custom_attributes</code> property. Most articles addressing this issue recommend customization via LayoutProcessor to change the attribute dataScope. I refer to this as “fixing” the dataScope, as it remains unclear why this fix is necessary for every single attribute.</li>
  <li>The Magento backend does not inherently know how or where to store extension attributes, so we need to configure Magento to handle this appropriately.</li>
</ol>

<p>Our implementation plan consists of the following steps:</p>

<ol>
  <li>Fix the frontend input dataScope so the selected value appears in the <code class="language-plaintext highlighter-rouge">address.customAttributes</code> property.</li>
  <li>Add a mixin to convert <code class="language-plaintext highlighter-rouge">customAttributes</code> into <code class="language-plaintext highlighter-rouge">extension_attributes</code> before submitting the shipping address to the backend.</li>
  <li>Define a new customer address extension attribute so Magento recognizes it.</li>
  <li>Add handler(s) to convert the extension attribute into an Address property.</li>
  <li>Add a field to the <code class="language-plaintext highlighter-rouge">quote_address</code> table to persist this property.</li>
</ol>

<p>At this stage, we need to configure Magento 2 to save this field to the quote whenever a customer selects an option during the checkout process. First, we will add a new field to the <code class="language-plaintext highlighter-rouge">quote_address</code> table where our custom field will be stored.</p>

<h3 id="step-1-fix-frontend-input-datascope">Step 1. Fix Frontend Input dataScope</h3>

<p>Despite Magento having an instrument to separate custom attributes, it is not utilized when Magento builds the jsLayout for these fields. As shown in <a href="https://github.com/magento/magento2/blob/2.4-develop/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php#L204">\Magento\Checkout\Block\Checkout\AttributeMerger::getFieldConfig</a>, the dataScope is always constructed as <code class="language-plaintext highlighter-rouge">$dataScopePrefix . '.' . $attributeCode</code>. Consequently, for a shipping address form, the dataScope would be <code class="language-plaintext highlighter-rouge">shippingAddress.custom_field</code> instead of the expected <code class="language-plaintext highlighter-rouge">shippingAddress.custom_attributes.custom_field</code>.</p>

<p>Let’s add a plugin to correct this issue.</p>

<p>First, we need to add a dependency on the <code class="language-plaintext highlighter-rouge">Magento_Checkout</code> module.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/module.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version = "1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Module/etc/module.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"SergiyNezbritskiy_CustomAddressField"</span> <span class="na">setup_version=</span><span class="s">"1.0.0"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;sequence&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Customer"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Checkout"</span><span class="nt">/&gt;</span> <span class="c">&lt;!-- This is what we've added --&gt;</span>
        <span class="nt">&lt;/sequence&gt;</span>
    <span class="nt">&lt;/module&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>Define our plugin in <code class="language-plaintext highlighter-rouge">di.xml</code>:</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/di.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:ObjectManager/etc/config.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Checkout\Block\Checkout\AttributeMerger"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_fix_data_scope"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Checkout\AttributeMergerPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>Within the plugin, fix the dataScope for our attribute:</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/Checkout/AttributeMergerPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Plugin\Checkout</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Checkout\Block\Checkout\AttributeMerger</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">AttributeMergerPlugin</span>
<span class="p">{</span>
    <span class="cd">/**
     * @see AttributeMerger::merge
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterMerge</span><span class="p">(</span><span class="kt">AttributeMerger</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$elements</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$providerName</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$dataScopePrefix</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$fields</span><span class="p">):</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">array_key_exists</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="nv">$result</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$oldScope</span> <span class="o">=</span> <span class="nv">$result</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">][</span><span class="s1">'dataScope'</span><span class="p">];</span>
            <span class="nv">$newScope</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="s1">'custom_attributes.custom_field'</span><span class="p">,</span> <span class="nv">$oldScope</span><span class="p">);</span>
            <span class="nv">$result</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">][</span><span class="s1">'dataScope'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$newScope</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This fix applies the dataScope correction for every address on the checkout page, including shippingAddress and billingAddresses (there are multiple billing addresses, as each payment method has its own UI component). You can add a breakpoint in this plugin to observe the execution flow.</p>

<p>Now, let’s flush the cache:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento cache:flush
</code></pre></div></div>

<h4 id="verification">Verification</h4>

<p>At this point, whenever an address is sent to the backend, you should be able to see the attribute value within the customAttributes property. For example:</p>

<p>When pressing “Next” on the shipping step:</p>

<p><img src="/assets/images/custom-address-field/04-fix-custom-attributes-data-scope.png" alt="" /></p>

<p>When saving the billing address:</p>

<p><img src="/assets/images/custom-address-field/05-fix-custom-attributes-data-scope.png" alt="" /></p>

<h3 id="step-11-fix-attribute-option-label">Step 1.1. Fix Attribute Option Label</h3>

<p>This is not part of our main flow, but you may have noticed that after saving the billing address, our option is displayed as an option ID instead of its label.</p>

<p><img src="/assets/images/custom-address-field/06-issue-with-saving-attribute-option-label.png" alt="" /></p>

<p>This occurs because customAttributes properties do not store labels (though they occasionally do, but typically do not). Magento expects us to define all labels for all options within the checkoutProvider UI component. We can push these options using the plugin we have already introduced.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/Checkout/AttributeMergerPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Plugin\Checkout</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Checkout\Block\Checkout\AttributeMerger</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">AttributeMergerPlugin</span>
<span class="p">{</span>
    <span class="cd">/**
     * @see AttributeMerger::merge
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterMerge</span><span class="p">(</span><span class="kt">AttributeMerger</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$elements</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$providerName</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$dataScopePrefix</span><span class="p">,</span> <span class="kt">array</span> <span class="nv">$fields</span><span class="p">):</span> <span class="kt">array</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">array_key_exists</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="nv">$result</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$oldScope</span> <span class="o">=</span> <span class="nv">$result</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">][</span><span class="s1">'dataScope'</span><span class="p">];</span>
            <span class="nv">$newScope</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="s1">'custom_attributes.custom_field'</span><span class="p">,</span> <span class="nv">$oldScope</span><span class="p">);</span>
            <span class="nv">$result</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">][</span><span class="s1">'dataScope'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$newScope</span><span class="p">;</span>
            <span class="nv">$result</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">][</span><span class="s1">'exports'</span><span class="p">][</span><span class="s1">'options'</span><span class="p">]</span><span class="o">=</span><span class="s1">'checkoutProvider:customAttributes.custom_field'</span><span class="p">;</span> <span class="c1">//this line has been added</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="verification-1">Verification</h4>

<p>The display should now be correct:</p>

<p><img src="/assets/images/custom-address-field/07-fix-issue-with-saving-attribute-option-label.png" alt="" /></p>

<h3 id="step-2-convert-customattributes-into-extension_attributes">Step 2. Convert customAttributes into extension_attributes</h3>

<p>This can be achieved by adding mixins to actions that handle addresses. While I have seen implementations for other actions, these two are sufficient for our purposes:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Magento_Checkout/js/action/set-shipping-information</code> - when proceeding to the billing step</li>
  <li><code class="language-plaintext highlighter-rouge">Magento_Checkout/js/action/place-order</code> - when placing the order</li>
</ol>

<p>For all these actions, we need to perform the same operation: convert <code class="language-plaintext highlighter-rouge">address.customAttributes.custom_field</code> into <code class="language-plaintext highlighter-rouge">address.extension_attributes.custom_field</code>. Let’s implement a reusable model for all mixins to avoid code duplication.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">view/frontend/web/js/model/extension-attribute-processor.js</code></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">define</span><span class="p">([</span>
    <span class="dl">'</span><span class="s1">jquery</span><span class="dl">'</span>
<span class="p">],</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">$</span><span class="p">)</span> <span class="p">{</span>

    <span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>

    <span class="k">return</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">attributeCode</span><span class="p">,</span> <span class="nx">address</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">extension_attributes</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">extension_attributes</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">customAttributes</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">customAttributes</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
        <span class="p">}</span>

        <span class="nx">$</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">customAttributes</span><span class="dl">'</span><span class="p">],</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">$</span><span class="p">.</span><span class="nx">isPlainObject</span><span class="p">(</span><span class="nx">value</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">key</span> <span class="o">=</span> <span class="nx">value</span><span class="p">[</span><span class="dl">'</span><span class="s1">attribute_code</span><span class="dl">'</span><span class="p">];</span>
                <span class="nx">value</span> <span class="o">=</span> <span class="nx">value</span><span class="p">[</span><span class="dl">'</span><span class="s1">value</span><span class="dl">'</span><span class="p">];</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">key</span> <span class="o">===</span> <span class="nx">attributeCode</span><span class="p">)</span> <span class="p">{</span>
                <span class="nx">address</span><span class="p">[</span><span class="dl">'</span><span class="s1">extension_attributes</span><span class="dl">'</span><span class="p">][</span><span class="nx">attributeCode</span><span class="p">]</span> <span class="o">=</span> <span class="nx">value</span><span class="p">;</span>
                <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">});</span>

    <span class="p">};</span>
<span class="p">});</span>

</code></pre></div></div>

<p>Now add our mixins through <code class="language-plaintext highlighter-rouge">requirejs-config.js</code>:</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">view/frontend/web/requirejs-config.js</code></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">config</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">mixins</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Magento_Checkout/js/action/set-shipping-information</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span>
                <span class="dl">'</span><span class="s1">SergiyNezbritskiy_CustomAddressField/js/action/set-extension-attributes-mixin</span><span class="dl">'</span><span class="p">:</span> <span class="kc">true</span>
            <span class="p">},</span>
            <span class="dl">'</span><span class="s1">Magento_Checkout/js/action/place-order</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span>
                <span class="dl">'</span><span class="s1">SergiyNezbritskiy_CustomAddressField/js/action/set-extension-attributes-mixin</span><span class="dl">'</span><span class="p">:</span> <span class="kc">true</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">view/frontend/web/js/action/set-extension-attributes-mixin.js</code></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">define</span><span class="p">([</span>
    <span class="dl">'</span><span class="s1">mage/utils/wrapper</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">Magento_Checkout/js/model/quote</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">SergiyNezbritskiy_CustomAddressField/js/model/extension-attribute-processor</span><span class="dl">'</span>
<span class="p">],</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">wrapper</span><span class="p">,</span> <span class="nx">quote</span><span class="p">,</span> <span class="nx">processExtensionAttribute</span><span class="p">)</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>

    <span class="k">return</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">setShippingInformationAction</span><span class="p">)</span> <span class="p">{</span>

        <span class="k">return</span> <span class="nx">wrapper</span><span class="p">.</span><span class="nx">wrap</span><span class="p">(</span><span class="nx">setShippingInformationAction</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">originalAction</span><span class="p">)</span> <span class="p">{</span>

            <span class="kd">let</span> <span class="nx">shippingAddress</span> <span class="o">=</span> <span class="nx">quote</span><span class="p">.</span><span class="nx">shippingAddress</span><span class="p">();</span>
            <span class="nx">processExtensionAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">custom_field</span><span class="dl">'</span><span class="p">,</span> <span class="nx">shippingAddress</span><span class="p">);</span>

            <span class="kd">let</span> <span class="nx">billingAddress</span> <span class="o">=</span> <span class="nx">quote</span><span class="p">.</span><span class="nx">billingAddress</span><span class="p">();</span>
            <span class="nx">processExtensionAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">custom_field</span><span class="dl">'</span><span class="p">,</span> <span class="nx">billingAddress</span><span class="p">);</span>

            <span class="k">return</span> <span class="nx">originalAction</span><span class="p">();</span>
        <span class="p">});</span>
    <span class="p">};</span>
<span class="p">});</span>
</code></pre></div></div>

<h4 id="verification-2">Verification</h4>

<p>At this point, you should be able to see that the Magento frontend is sending extension attributes to the backend.</p>

<p><img src="/assets/images/custom-address-field/08-convert-custom-attributes-into-extension-attributes.png" alt="" /></p>

<p>However, the backend will now return an error stating that CustomField is not supported. This is expected because we have not yet registered this extension attribute. Let’s proceed to the next step to resolve this.</p>

<h3 id="step-3-define-extension-attribute">Step 3. Define Extension Attribute</h3>

<p>For this, we simply need to add an <code class="language-plaintext highlighter-rouge">etc/extension_attributes.xml</code> file. Before doing so, let’s add the Magento_Quote module to the sequence:</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/module.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version = "1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Module/etc/module.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"SergiyNezbritskiy_CustomAddressField"</span> <span class="na">setup_version=</span><span class="s">"1.0.0"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;sequence&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Customer"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Checkout"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;module</span> <span class="na">name=</span><span class="s">"Magento_Quote"</span><span class="nt">/&gt;</span><span class="c">&lt;!-- this line has been added --&gt;</span>
        <span class="nt">&lt;/sequence&gt;</span>
    <span class="nt">&lt;/module&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/extension_attributes.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" ?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Api/etc/extension_attributes.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;extension_attributes</span> <span class="na">for=</span><span class="s">"Magento\Quote\Api\Data\AddressInterface"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;attribute</span> <span class="na">code=</span><span class="s">"custom_field"</span> <span class="na">type=</span><span class="s">"string"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/extension_attributes&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>After changing the sequence and defining new extension attributes, we need to run the <code class="language-plaintext highlighter-rouge">setup:upgrade</code> command:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
</code></pre></div></div>

<h4 id="verification-3">Verification</h4>

<p>First, the frontend error should be resolved. Additionally, the QuoteAddressInterface should be generated with <code class="language-plaintext highlighter-rouge">setCustomField</code> and <code class="language-plaintext highlighter-rouge">getCustomField</code> methods. Check the <code class="language-plaintext highlighter-rouge">generated/code/Magento/Quote/Api/Data/AddressExtensionInterface.php</code> file, which should contain the following:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
 * @return string|null
 */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getCustomField</span><span class="p">();</span>

<span class="cd">/**
 * @param string $customField
 * @return $this
 */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">setCustomField</span><span class="p">(</span><span class="nv">$customField</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="step-4-convert-extension-attribute-into-address-data">Step 4. Convert Extension Attribute into Address Data</h3>

<p>At this point, if we set a breakpoint in the <code class="language-plaintext highlighter-rouge">\Magento\Checkout\Model\ShippingInformationManagement::saveAddressInformation</code> method and submit the checkout shipping step, we can see that <code class="language-plaintext highlighter-rouge">$addressInformation</code> contains our extension attribute. Our goal is to make Magento convert it into address data every time the Address object is initialized or extension attributes are set.</p>

<p>There are several scenarios for initializing a Quote\Address object with extension_attributes:</p>

<ol>
  <li>We can create an object and call <code class="language-plaintext highlighter-rouge">setExtensionAttributes</code></li>
  <li>We can create an object and call <code class="language-plaintext highlighter-rouge">setData</code> with an array argument containing <code class="language-plaintext highlighter-rouge">extension_attributes</code> or with the key <code class="language-plaintext highlighter-rouge">extension_attributes</code></li>
  <li>We can pass <code class="language-plaintext highlighter-rouge">$data</code> with <code class="language-plaintext highlighter-rouge">extension_attributes</code> as an argument to the constructor. In this case, the <code class="language-plaintext highlighter-rouge">setData</code> method will be called during object initialization as well.</li>
</ol>

<p>Therefore, we need to add plugins for the <code class="language-plaintext highlighter-rouge">setExtensionAttributes</code> and <code class="language-plaintext highlighter-rouge">setData</code> methods to cover all possible scenarios.</p>

<p>Additionally, let’s ensure that every time we retrieve extension attributes, our custom_field is always present. We’ll add an after plugin for the <code class="language-plaintext highlighter-rouge">getExtensionAttributes</code> method.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/di.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:ObjectManager/etc/config.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Checkout\Block\Checkout\AttributeMerger"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_fix_data_scope"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Checkout\AttributeMergerPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="c">&lt;!-- This plugin has been added --&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Quote\Model\Quote\Address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_convert_extension_attributes"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Quote\AddressPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/Quote/AddressPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Plugin\Quote</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Quote\Api\Data\AddressExtension</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Quote\Api\Data\AddressExtensionInterface</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Quote\Api\Data\AddressExtensionInterfaceFactory</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Quote\Model\Quote\Address</span><span class="p">;</span>

<span class="k">readonly</span> <span class="kd">class</span> <span class="nc">AddressPlugin</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span><span class="k">private</span> <span class="kt">AddressExtensionInterfaceFactory</span> <span class="nv">$factory</span><span class="p">)</span>
    <span class="p">{</span>
    <span class="p">}</span>

    <span class="cd">/**
     * @see Address::setData()
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterSetData</span><span class="p">(</span><span class="kt">Address</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">Address</span> <span class="nv">$result</span><span class="p">,</span> <span class="nv">$key</span><span class="p">,</span> <span class="nv">$value</span> <span class="o">=</span> <span class="kc">null</span><span class="p">):</span> <span class="kt">Address</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nb">is_array</span><span class="p">(</span><span class="nv">$key</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nb">array_key_exists</span><span class="p">(</span><span class="s1">'extension_attributes'</span><span class="p">,</span> <span class="nv">$key</span><span class="p">))</span> <span class="p">{</span>
            <span class="cd">/** @var AddressExtensionInterface $value */</span>
            <span class="nv">$value</span> <span class="o">=</span> <span class="nv">$key</span><span class="p">[</span><span class="s1">'extension_attributes'</span><span class="p">];</span>
            <span class="nv">$key</span> <span class="o">=</span> <span class="s1">'extension_attributes'</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="nv">$key</span> <span class="o">===</span> <span class="s1">'extension_attributes'</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">convert</span><span class="p">(</span><span class="nv">$result</span><span class="p">,</span> <span class="nv">$value</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="cd">/**
     * @see Address::setExtensionAttributes()
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterSetExtensionAttributes</span><span class="p">(</span><span class="kt">Address</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">Address</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">AddressExtensionInterface</span> <span class="nv">$extensionAttributes</span><span class="p">):</span> <span class="kt">Address</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">convert</span><span class="p">(</span><span class="nv">$result</span><span class="p">,</span> <span class="nv">$extensionAttributes</span><span class="p">);</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="cd">/**
     * @see Address::getExtensionAttributes()
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterGetExtensionAttributes</span><span class="p">(</span><span class="kt">Address</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">?AddressExtensionInterface</span> <span class="nv">$result</span><span class="p">):</span> <span class="kt">?AddressExtensionInterface</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$result</span> <span class="o">||</span> <span class="o">!</span><span class="nv">$result</span><span class="o">-&gt;</span><span class="nf">getCustomField</span><span class="p">())</span> <span class="p">{</span>
            <span class="nv">$customField</span> <span class="o">=</span> <span class="nv">$subject</span><span class="o">-&gt;</span><span class="nf">getData</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="nv">$customField</span><span class="p">)</span> <span class="p">{</span>
                <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">ensureExtensionAttributes</span><span class="p">(</span><span class="nv">$result</span><span class="p">);</span>
                <span class="nv">$result</span><span class="o">-&gt;</span><span class="nf">setCustomField</span><span class="p">(</span><span class="nv">$customField</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">function</span> <span class="n">convert</span><span class="p">(</span><span class="kt">Address</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">AddressExtensionInterface</span> <span class="nv">$extensionAttributes</span><span class="p">):</span> <span class="kt">void</span>
    <span class="p">{</span>
        <span class="nv">$newValue</span> <span class="o">=</span> <span class="nv">$extensionAttributes</span><span class="o">-&gt;</span><span class="nf">getCustomField</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$newValue</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$result</span><span class="o">-&gt;</span><span class="nf">setData</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="nv">$newValue</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">function</span> <span class="n">ensureExtensionAttributes</span><span class="p">(</span><span class="kt">?AddressExtensionInterface</span> <span class="nv">$result</span><span class="p">):</span> <span class="kt">?AddressExtensionInterface</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$result</span><span class="p">)</span> <span class="p">{</span>
            <span class="nv">$result</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">factory</span><span class="o">-&gt;</span><span class="nf">create</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, if we navigate to <code class="language-plaintext highlighter-rouge">\Magento\Webapi\Controller\Rest\SynchronousRequestProcessor::process</code> and set a breakpoint after <code class="language-plaintext highlighter-rouge">$inputParams</code> are resolved, we should see that <code class="language-plaintext highlighter-rouge">$inputParams[1]-&gt;_data["shipping_address"]-&gt;_data["custom_field"]</code> is set.</p>

<h3 id="step-5-add-field-to-quote_address-table">Step 5. Add Field to quote_address Table</h3>

<p>The final step is to persist this address data to the database. Let’s add the corresponding field:</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/db_schema.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;schema</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;table</span> <span class="na">name=</span><span class="s">"quote_address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;column</span> <span class="na">xsi:type=</span><span class="s">"varchar"</span> <span class="na">name=</span><span class="s">"custom_field"</span> <span class="na">nullable=</span><span class="s">"true"</span> <span class="na">length=</span><span class="s">"255"</span> <span class="na">comment=</span><span class="s">"Custom Address Field"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/table&gt;</span>
<span class="nt">&lt;/schema&gt;</span>
</code></pre></div></div>

<p>Upgrade the database:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
</code></pre></div></div>

<h4 id="verification-4">Verification</h4>

<p>At this moment, you should be able to see your field in the <code class="language-plaintext highlighter-rouge">quote_address</code> table, and the data should be written to <code class="language-plaintext highlighter-rouge">quote_address.custom_field</code> while processing the order. After placing the order, both addresses should have this value populated.</p>]]></content><author><name>Sergiy Nezbritskiy</name></author><category term="Magento 2" /><category term="Checkout" /><category term="Development" /><summary type="html"><![CDATA[Challenges]]></summary></entry><entry><title type="html">Magento 2: Adding Custom Shipping Address Field Tutorial (Part 3 - Order Placement and Enhancements)</title><link href="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-3.html" rel="alternate" type="text/html" title="Magento 2: Adding Custom Shipping Address Field Tutorial (Part 3 - Order Placement and Enhancements)" /><published>2025-10-10T00:00:00+00:00</published><updated>2025-10-10T00:00:00+00:00</updated><id>https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-3</id><content type="html" xml:base="https://sergiynezbritskiy.github.io/2025/10/10/custom-address-field-part-3.html"><![CDATA[<h3 id="challenges">Challenges</h3>

<p>We now have our custom attribute data stored in the quote address, but we need to configure Magento to convert this data into the sales order address. Additionally, there are several minor issues to address:</p>

<ul>
  <li>Attempting to save the address to the address book generates an error</li>
  <li>Creating an account after guest checkout does not handle our custom attribute</li>
  <li>Attempting to place an order as a guest generates an error</li>
</ul>

<h3 id="step-1-introduce-sales_order_addresscustom_field-column">Step 1. Introduce sales_order_address.custom_field Column</h3>

<p>Update the <code class="language-plaintext highlighter-rouge">db_schema.xml</code> file:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;schema</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;table</span> <span class="na">name=</span><span class="s">"quote_address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;column</span> <span class="na">xsi:type=</span><span class="s">"varchar"</span> <span class="na">name=</span><span class="s">"custom_field"</span> <span class="na">nullable=</span><span class="s">"true"</span> <span class="na">length=</span><span class="s">"255"</span> <span class="na">comment=</span><span class="s">"Custom Address Field"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/table&gt;</span>
    <span class="c">&lt;!-- add new column --&gt;</span>
    <span class="nt">&lt;table</span> <span class="na">name=</span><span class="s">"sales_order_address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;column</span> <span class="na">xsi:type=</span><span class="s">"varchar"</span> <span class="na">name=</span><span class="s">"custom_field"</span> <span class="na">nullable=</span><span class="s">"true"</span> <span class="na">length=</span><span class="s">"255"</span> <span class="na">comment=</span><span class="s">"Custom Address Field"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/table&gt;</span>
<span class="nt">&lt;/schema&gt;</span>
</code></pre></div></div>

<p>Run the database upgrade:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento setup:upgrade
</code></pre></div></div>

<p>In theory, there is a class responsible for copying data from <code class="language-plaintext highlighter-rouge">quote_address</code> to <code class="language-plaintext highlighter-rouge">sales_order_address</code> called <a href="https://github.com/magento/magento2/blob/2.4-develop/app/code/Magento/Quote/Model/Quote/Address/ToOrderAddress.php">\Magento\Quote\Model\Quote\Address\ToOrderAddress</a>, which utilizes <a href="https://github.com/magento/magento2/blob/2.4-develop/lib/internal/Magento/Framework/DataObject/Copy.php">\Magento\Framework\DataObject\Copy</a>. We need to add copy instructions to the <code class="language-plaintext highlighter-rouge">fieldset.xml</code> file for <code class="language-plaintext highlighter-rouge">sales_convert_quote_address</code>.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/fieldset.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Data/etc/fieldset.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;scope</span> <span class="na">id=</span><span class="s">"global"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;fieldset</span> <span class="na">id=</span><span class="s">"sales_convert_quote_address"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">"custom_field"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_order_address"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;/field&gt;</span>
        <span class="nt">&lt;/fieldset&gt;</span>
    <span class="nt">&lt;/scope&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p>However, this approach does not work as expected, so I added an additional plugin to this class and handled it manually.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/di.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:ObjectManager/etc/config.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Checkout\Block\Checkout\AttributeMerger"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_fix_data_scope"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Checkout\AttributeMergerPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Quote\Model\Quote\Address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_convert_extension_attributes"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Quote\AddressPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="c">&lt;!-- added new plugin --&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Quote\Model\Quote\Address\ToOrderAddress"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_quote_to_order"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\QuoteAddressToOrderAddress\ToOrderAddressConverterPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/QuoteAddressToOrderAddress/ToOrderAddressConverterPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Plugin\QuoteAddressToOrderAddress</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Quote\Model\Quote\Address</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Quote\Model\Quote\Address\ToOrderAddress</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Magento\Sales\Api\Data\OrderAddressInterface</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">ToOrderAddressConverterPlugin</span>
<span class="p">{</span>
    <span class="cd">/**
     * @see ToOrderAddress::convert
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">afterConvert</span><span class="p">(</span><span class="kt">ToOrderAddress</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">OrderAddressInterface</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">Address</span> <span class="nv">$object</span><span class="p">):</span> <span class="kt">OrderAddressInterface</span>
    <span class="p">{</span>
        <span class="cd">/** @var \Magento\Sales\Model\Order\Address $result */</span>
        <span class="nv">$result</span><span class="o">-&gt;</span><span class="nf">setData</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="nv">$object</span><span class="o">-&gt;</span><span class="nf">getData</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">));</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Clean the cache before proceeding:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>php bin/magento cache:clean
</code></pre></div></div>

<h4 id="verification">Verification</h4>

<p>At this point, you should be able to see your attribute on the order view page and in the <code class="language-plaintext highlighter-rouge">sales_order_address.custom_field</code> column.</p>

<h3 id="step-2-fix-saving-address-to-address-book-as-a-registered-user">Step 2. Fix Saving Address to Address Book as a Registered User</h3>

<p>Again, we need to configure the <code class="language-plaintext highlighter-rouge">\Magento\Framework\DataObject\Copy</code> object.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/fieldset.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Data/etc/fieldset.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;scope</span> <span class="na">id=</span><span class="s">"global"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;fieldset</span> <span class="na">id=</span><span class="s">"sales_convert_quote_address"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">"custom_field"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_order_address"</span><span class="nt">/&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_customer_address"</span><span class="nt">/&gt;</span><span class="c">&lt;!-- this line has been added --&gt;</span>
            <span class="nt">&lt;/field&gt;</span>
        <span class="nt">&lt;/fieldset&gt;</span>
    <span class="nt">&lt;/scope&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<h4 id="verification-1">Verification</h4>

<p>At this point, the option to save the address to the address book should work correctly for both shipping and billing addresses.</p>

<h3 id="step-3-fix-placing-order-as-a-guest-user">Step 3. Fix Placing Order as a Guest User</h3>

<p>Now let’s address the guest user scenario. For guest users, the data from quote address to sales order address is copied from custom attributes. Let’s add a plugin to set them whenever they are called.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/Quote/AddressPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
 * @see \Magento\Quote\Model\Quote\Address::getCustomAttributes
 */</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">afterGetCustomAttributes</span><span class="p">(</span><span class="err">\</span><span class="nc">Magento\Quote\Model\Quote\Address</span> <span class="nv">$subject</span><span class="p">,</span> <span class="k">array</span> <span class="nv">$customAttributes</span><span class="p">)</span><span class="o">:</span> <span class="k">array</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">array_key_exists</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">,</span> <span class="nv">$customAttributes</span><span class="p">))</span> <span class="p">{</span>
        <span class="nv">$customAttributes</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="err">\</span><span class="nf">Magento\Framework\Api\AttributeValue</span><span class="p">([</span>
            <span class="s1">'attribute_code'</span> <span class="o">=&gt;</span> <span class="s1">'custom_field'</span><span class="p">,</span>
            <span class="s1">'value'</span> <span class="o">=&gt;</span> <span class="nv">$subject</span><span class="o">-&gt;</span><span class="nf">getData</span><span class="p">(</span><span class="s1">'custom_field'</span><span class="p">),</span>
        <span class="p">]);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nv">$customAttributes</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="verification-2">Verification</h4>

<p>At this point, you should be able to see the success page after placing the order.</p>

<p><img src="/assets/images/custom-address-field/09-success-page.png" alt="" /></p>

<h3 id="step-4-fix-creating-account-after-guest-order">Step 4. Fix Creating Account After Guest Order</h3>

<p>The final issue to address is the “Create an Account” button on the success page. Currently, after clicking that button and attempting to register an account, we will receive an error stating that “Custom Field” is a required value.</p>

<p>How does this work? When we click the “Create an Account” button, Magento saves customer data (including address data) into session storage. You can see this in <code class="language-plaintext highlighter-rouge">\Magento\Customer\Model\Delegation\Storage::storeNewOperation</code>, specifically in the line <code class="language-plaintext highlighter-rouge">$this-&gt;session-&gt;setCustomerFormData($customerData)</code>. What actually happens is that Magento serializes the object and then unserializes it when we submit the registration form by calling the non-existent <code class="language-plaintext highlighter-rouge">getDelegatedNewCustomerData</code> method from the Session object. Our custom data gets lost during serialization. Let’s fix this by adding one more plugin.</p>

<p>First, we need to configure Magento (the <code class="language-plaintext highlighter-rouge">\Magento\Framework\DataObject\Copy</code> object) on how to handle <code class="language-plaintext highlighter-rouge">custom_field</code> when copying sales order address into customer address.</p>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/fieldset.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:Data/etc/fieldset.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;scope</span> <span class="na">id=</span><span class="s">"global"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;fieldset</span> <span class="na">id=</span><span class="s">"sales_convert_quote_address"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">"custom_field"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_order_address"</span><span class="nt">/&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_customer_address"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;/field&gt;</span>
        <span class="nt">&lt;/fieldset&gt;</span>
        <span class="c">&lt;!-- This section has been added --&gt;</span>
        <span class="nt">&lt;fieldset</span> <span class="na">id=</span><span class="s">"order_address"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">"custom_field"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;aspect</span> <span class="na">name=</span><span class="s">"to_customer_address"</span><span class="nt">/&gt;</span>
            <span class="nt">&lt;/field&gt;</span>
        <span class="nt">&lt;/fieldset&gt;</span>
    <span class="nt">&lt;/scope&gt;</span>
<span class="nt">&lt;/config&gt;</span>
</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">etc/di.xml</code></p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0"?&gt;</span>
<span class="nt">&lt;config</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xsi:noNamespaceSchemaLocation=</span><span class="s">"urn:magento:framework:ObjectManager/etc/config.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Checkout\Block\Checkout\AttributeMerger"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_fix_data_scope"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Checkout\AttributeMergerPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Quote\Model\Quote\Address"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_convert_extension_attributes"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Quote\AddressPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Quote\Model\Quote\Address\ToOrderAddress"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_quote_to_order"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\QuoteAddressToOrderAddress\ToOrderAddressConverterPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
    <span class="c">&lt;!-- This section has been added --&gt;</span>
    <span class="nt">&lt;type</span> <span class="na">name=</span><span class="s">"\Magento\Customer\Model\Session"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;plugin</span> <span class="na">name=</span><span class="s">"sn_restore_custom_attributes"</span> <span class="na">type=</span><span class="s">"SergiyNezbritskiy\CustomAddressField\Plugin\Session\OrderToCustomerPlugin"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/type&gt;</span>
<span class="nt">&lt;/config&gt;</span>

</code></pre></div></div>

<p><strong>File:</strong> <code class="language-plaintext highlighter-rouge">Plugin/Session/OrderToCustomerPlugin.php</code></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span>

<span class="k">declare</span><span class="p">(</span><span class="n">strict_types</span><span class="o">=</span><span class="mi">1</span><span class="p">);</span>

<span class="kn">namespace</span> <span class="nn">SergiyNezbritskiy\CustomAddressField\Plugin\Session</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">Magento\Customer\Model\Session</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">OrderToCustomerPlugin</span>
<span class="p">{</span>
    <span class="cd">/**
     * @see Session::__call
     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">after__call</span><span class="p">(</span><span class="kt">Session</span> <span class="nv">$subject</span><span class="p">,</span> <span class="kt">mixed</span> <span class="nv">$result</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$method</span><span class="p">):</span> <span class="kt">mixed</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nv">$method</span> <span class="o">===</span> <span class="s1">'getDelegatedNewCustomerData'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nb">is_array</span><span class="p">(</span><span class="nv">$result</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nb">array_key_exists</span><span class="p">(</span><span class="s1">'addresses'</span><span class="p">,</span> <span class="nv">$result</span><span class="p">))</span> <span class="p">{</span>
                <span class="k">foreach</span> <span class="p">(</span><span class="nv">$result</span><span class="p">[</span><span class="s1">'addresses'</span><span class="p">]</span> <span class="k">as</span> <span class="o">&amp;</span><span class="nv">$address</span><span class="p">)</span> <span class="p">{</span>
                    <span class="nv">$address</span><span class="p">[</span><span class="s1">'custom_attributes'</span><span class="p">][</span><span class="s1">'custom_field'</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$address</span><span class="p">[</span><span class="s1">'custom_field'</span><span class="p">];</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$result</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Sergiy Nezbritskiy</name></author><category term="Magento 2" /><category term="Checkout" /><category term="Development" /><summary type="html"><![CDATA[Challenges]]></summary></entry></feed>