✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

EcosystemIntermediate●6 min read

"Cannot read properties of undefined (reading 'attributes')" in Strapi

●September 9, 2026
"Cannot read properties of undefined (reading 'attributes')" in Strapi

Strapi refuses to start:

debug: ⛔️ Server wasn't able to start properly.
error: Cannot read properties of undefined (reading 'attributes')
TypeError: Cannot read properties of undefined (reading 'attributes')
    at Object.getNonVisibleAttributes (.../@strapi/utils/lib/content-types.js)
    at getNestedFields (.../@strapi/admin/server/services/content-type.js)
    ...

The most upvoted advice on this error, historically, has been "recreate the project". You do not need to.

What it means

Strapi is walking your content types to build the admin panel's field configuration. For each attribute it needs to resolve the target schema, either a component or the content type on the other end of a relation, and read its attributes.

One of those lookups returned undefined, and the code then tried to read .attributes off nothing.

Translation: a content type references something that does not exist. Almost always a component; occasionally a relation, a dynamic zone entry, or a custom field whose plugin is not installed.

Find the broken reference

Every component reference vs. every component file

This one-liner catches the large majority of cases:

grep -rho '"component": "[^"]*"' src/api src/components \
  | sed 's/.*: "//; s/"//' | sort -u \
  | while read c; do
      f="src/components/${c/./\/}.json"
      [ -f "$f" ] || echo "MISSING: $c  (expected $f)"
    done

A component named shared.seo must exist at src/components/shared/seo.json. Anything printed is a reference with no file behind it.

Dynamic zones

Dynamic zones list their allowed components in a components array, which the grep above does not cover:

"blocks": {
  "type": "dynamiczone",
  "components": ["layout.hero", "layout.cta", "shared.quote"]
}

Check those too:

grep -rA10 '"type": "dynamiczone"' src/api src/components \
  | grep -o '"[a-z0-9-]*\.[a-z0-9-]*"' | tr -d '"' | sort -u \
  | while read c; do
      f="src/components/${c/./\/}.json"
      [ -f "$f" ] || echo "MISSING (dynamiczone): $c"
    done

Relations pointing at deleted content types

grep -rho '"target": "[^"]*"' src/api src/components \
  | sed 's/.*: "//; s/"//' | sort -u

Each api::foo.foo target needs src/api/foo/content-types/foo/schema.json to exist. Targets starting plugin:: refer to plugins, so plugin::users-permissions.user is fine as long as that plugin is installed.

Nested components

A component can reference another component. If layout.hero includes shared.button and shared/button.json was deleted, the failure surfaces when the parent is loaded, so the error will point you at hero rather than at the actual missing file. The first grep catches this because it searches src/components as well as src/api.

Custom fields from uninstalled plugins

grep -rn '"customField"' src/api src/components

A "customField": "plugin::color-picker.color" on a content type whose plugin is no longer in package.json produces the same class of failure. Either reinstall the plugin or remove the attribute.

The migration case

This error is common right after a v3 → v4 or v4 → v5 migration, and the reason is structural: components moved location and naming conventions changed between major versions. Codemods handle most of it and reliably miss edge cases. As the original forum thread found, the root cause there was components that were never moved to their v4 location.

After running any migration tool:

  1. Run the checks above before doing anything else.
  2. Diff src/components/ against the old project and confirm every file arrived.
  3. Open a few schema.json files by hand and confirm the shape looks like the current version's.

For a v4 → v5 move specifically, use the upgrade tool and read the migration guide, because several attribute-level changes are documented there and are easy to miss.

Fixing it

Once you know which reference is broken, you have two options.

Restore the missing file

If it was deleted by accident, get it back:

git log --diff-filter=D --name-only -- src/components/
git checkout <commit>^ -- src/components/shared/seo.json

This is the better outcome, because the database table for that component still exists and the content in it is preserved.

Remove the reference

If the component is genuinely gone, edit the content type's schema.json and delete the attribute:

{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": { "singularName": "article", "pluralName": "articles", "displayName": "Article" },
  "options": { "draftAndPublish": true },
  "attributes": {
    "title": { "type": "string" },
    "body": { "type": "richtext" }
    // "seo": { "type": "component", "component": "shared.seo" }   ← removed
  }
}

Understand what this costs. On the next startup, Strapi's schema sync compares your content types with the database and drops tables, columns, and indexes it previously managed that no longer match. Removing the attribute means the component's data is deleted, without a prompt, in development and production alike.

If the data matters:

  • Take a database backup first.
  • Or set forceMigration: false in config/database settings to skip drop operations, retrieve what you need, then decide. Note the caveat in the docs: the new schema is still recorded as the reference, so an object whose deletion was skipped stops being tracked and will not be dropped later if you set the flag back.

Getting a name out of the error

The stack trace does not tell you which content type failed. A temporary log in register will:

export default {
  register({ strapi }) {
    for (const [uid, ct] of Object.entries(strapi.contentTypes)) {
      for (const [name, attr] of Object.entries<any>(ct.attributes ?? {})) {
        if (attr.type === 'component' && !strapi.components[attr.component]) {
          strapi.log.error(`Broken component reference: ${uid}.${name} → ${attr.component}`);
        }
        if (attr.type === 'dynamiczone') {
          for (const c of attr.components ?? []) {
            if (!strapi.components[c]) {
              strapi.log.error(`Broken dynamiczone reference: ${uid}.${name} → ${c}`);
            }
          }
        }
      }
    }
  },
  bootstrap() {},
};

This runs early enough to print before the admin service crashes, so you get names instead of frames. Remove it once you have the answer.

Recovering from a boot loop

You cannot use the admin panel when Strapi will not start, so:

  1. git status and git diff: the change is usually right there.
  2. git stash to test whether uncommitted work caused it.
  3. Move suspect files out of the tree rather than deleting them: mv src/components/shared/seo.json /tmp/.
  4. Clear stale build output: rm -rf dist .cache build .strapi && npm run build.

Preventing it

  • Never delete a component file by hand. Use the Content-Type Builder, which removes references at the same time.
  • Commit src/components/ and src/api/. These are code, and a schema diff in review catches broken references before they reach anyone else.
  • Boot Strapi in CI against a throwaway database. Every error in this post fails at startup, so a job that starts the server catches all of them at pull-request time.
  • After any migration, run the grep checks above before you start debugging anything else.

ERR_INVALID_ARG_TYPE: The "path" argument must be of type string is the sibling of this error: same underlying category, different failure point in the loader.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

The True TCO of Self-Hosting a Headless CMS
Ecosystem·12 min read

The True TCO of Self-Hosting a Headless CMS

Discover the real cost of self-hosting Strapi 5: infrastructure, DevOps labor, security, migrations, and hidden costs—with a practical TCO checklist.

·September 24, 2026
How Do You Preserve SEO Rankings During a CMS Migration?
Ecosystem·13 min read

How Do You Preserve SEO Rankings During a CMS Migration?

A developer's CMS migration SEO checklist: URL mapping, 301 redirects, on-page parity, robots.txt, redirect testing, and post-launch monitoring.

·September 24, 2026
What Is a Content Agent? Agentic AI for CMS Workflows Explained
Ecosystem·24 min read

What Is a Content Agent? Agentic AI for CMS Workflows Explained

Learn what a content agent is, how the observe-plan-act-evaluate loop works, and when agentic AI makes sense for your CMS workflows and content operations.

·September 24, 2026