---
title: "Relations"
manual: "TCA_API"
version: "2.2"
permalink: "https://docs.typo3.org/permalink/maikschneider/tca-api:relations@2.2"
source: "Configuration/Relations.rst"
rendered: "2026-10-12T13:17:40+00:00"
---

# Relations {#relations}

Relations are resolved automatically from the TCA schema. By default, related
records are serialized as **plain IRI strings**:

```json
{
    "color_id": "/_api/colors/1",
    "categories": [
        "/_api/categories/5",
        "/_api/categories/8"
    ]
}
```

This format is compatible with API Platform Admin and other Hydra clients that
resolve IRIs on demand. To inline the full related record instead, see
[Embedding related records](https://docs.typo3.org/permalink/maikschneider/tca-api:embedding-related-records@2.2) below.

An IRI is only rendered when it dereferences, i.e. when the related table has a
registered resource with the `show` operation. A relation to any other table
renders the related record's `uid` instead (`"color_id": 1`). In a group
column that may reference several tables, where a bare uid would not say which
table it belongs to, such an item renders as `{"@type": "…", "uid": 1}`.

## Embedding related records {#embedding-related-records}

Add `'embed' => true` to a column to inline the full related record instead of
a stub:

```php
'columns' => [
    'color_id'   => ['groups' => ['list', 'show'], 'embed' => true],
    'categories' => ['groups' => ['list', 'show'], 'embed' => true],
],
```

In default mode, `embed` alone is enough — it does **not** trigger explicit
mode:

```php
'columns' => [
    'color_id' => ['embed' => true],
],
```

Response with embedded data:

```json
{
    "color": {
        "@id": "/_api/colors/1",
        "@type": "Color",
        "uid": 1,
        "name": "Red",
        "hex": "#ff0000"
    },
    "categories": [
        {
            "@id": "/_api/sys-categories/5",
            "@type": "SysCategory",
            "uid": 5,
            "title": "News"
        }
    ]
}
```

## Controlling recursion depth {#controlling-recursion-depth}

Control recursion depth with `'embed' => ['depth' => 2]`. The default depth
is 1.

```php
'columns' => [
    'color_id' => ['embed' => ['depth' => 2]],
],
```

> [!IMPORTANT]
> The related resource must be registered in the `ApiRegistry` for embedding
> to work.

## Multi-table group fields {#multi-table-group-fields}

TCA `type=group` fields with multiple allowed tables store relations in the
prefixed `tablename_uid` format (e.g. `pages_1,sys_file_3`). These fields
support embedding just like single-table relations:

```php
// TCA definition
'related_items' => [
    'label' => 'Related Items',
    'config' => [
        'type' => 'group',
        'allowed' => 'tx_myext_domain_model_article,tx_myext_domain_model_color',
    ],
],

// API resource configuration
'columns' => [
    'related_items' => ['groups' => ['list', 'show'], 'embed' => true],
],
```

The embedded response contains full objects from different tables, each with its
own `@type` and fields:

```json
{
    "related_items": [
        {
            "@id": "/_api/articles/201",
            "@type": "Article",
            "uid": 201,
            "title": "Related Article"
        },
        {
            "@id": "/_api/colors/1",
            "@type": "Color",
            "uid": 1,
            "name": "Red"
        }
    ]
}
```

Each referenced table is resolved to its registered API resource. When no
resource is registered for a table, a default-mode config is synthesized
automatically (all TCA columns exposed). Its `@type` is derived from the
table name (`tx_news_domain_model_tag` → `TxNewsDomainModelTag`) unless the
column sets `resourceType`.

An embedded record that has no item endpoint — from an unregistered table, or
from a resource without the `show` operation — carries no `@id`. It is a
JSON-LD blank node: an `@id` would point at a URL that answers `404`.

Bulk preloading is fully supported: collection requests issue one `SELECT`
per referenced table regardless of how many parent rows exist, avoiding N+1
queries.

## Writing nested objects {#writing-nested-objects}

When a create or update request contains a related record as an **object**
(rather than a UID), TCA_API creates the child record in-line. The child
table must have a registered resource in the API registry; a nested object for
an unregistered table is rejected with `422` and an `UNRESOLVABLE_RELATION`
violation naming the column and the table.

Passing an existing UID or IRI never needs a registered child resource, so
linking is unaffected.

### Nested writes without a child resource {#nested-writes-without-a-child-resource}

A child table that should not be reachable as a resource of its own does not
need one. Declare `nestedWrite` on the parent column instead — it states the
role required to create through that relation, and the child's shape is derived
from TCA:

```php
use MaikSchneider\TcaApi\Enum\AccessRole;

'columns' => [
    'note_id' => [
        'groups'      => ['list', 'show', 'create'],
        'nestedWrite' => AccessRole::FE_USER,
    ],
],
```

`nestedWrite` accepts the same shapes as a `security` entry: an
`AccessRole`, an `[AccessRole, groupIds]` tuple, or a
`[class-string, method-string]` callable.

When the child table *is* registered, `nestedWrite` still wins for the access
check — who may write through a relation is a narrower question than what the
child resource permits on its own endpoint — while columns, validation and
ownership continue to come from the registered resource.

```json
{
    "title": "My Article",
    "color_id": { "name": "Red" }
}
```

By default the child resource is looked up by the **foreign table** name from
TCA. When multiple resources are registered for the same DB table, use the
`resourceName` column option to pin the lookup to a specific resource:

```php
'columns' => [
    'color_id' => [
        'groups'       => ['list', 'show', 'create', 'update'],
        'resourceName' => 'colors',   // look up "colors" resource, not by table
    ],
],
```

This also controls which resource's `security.create` role is enforced on the
nested write and which resource's column config is used for validation.

> [!NOTE]
> If `resourceName` is set but no matching resource is registered, TCA_API
> throws an `\InvalidArgumentException` at load time (validated in
> `ApiDefinitionLoader`).

## Supported relation types {#supported-relation-types}

| TCA type | Default output | Embedding |
| --- | --- | --- |
| `select` / `group` | UID list (`1,2,3`) — single table → IRI strings | Yes |
| `select` / `group` | Prefixed list (`table_uid`) — multi-table → IRI strings | Yes |
| `inline` / `select` | `foreign_field` back-reference → IRI strings | Yes |
| Any + `MM` | Intermediate MM table → IRI strings | Yes |
| `type=group` \+ `MM` | Column holds count, relations in MM → IRI strings | Yes |
