Morphing lets one record exist in multiple object contexts without duplicating
data. This post explains how **primary object** and **morph objects** work,
how object-level `extendsTo` configuration drives automatic morphing, and how
to model a shared **person** record that is visible as both **contact** and
**candidate**.

## Why morphing exists

In many Caraer setups, the same real-world entity appears in more than one
object:

- A person can be a **candidate** and later also a **contact**
- A lead can become a **candidate** without copying `firstname`, `email`, and
other shared fields


Without morphing, you would duplicate records or maintain fragile cross-links.
Morphing solves this by keeping **one record** (one UUID, one set of property
values) while making it visible in multiple object contexts.

## The mental model

Every record has:

| Concept | Property | Meaning |
|  --- | --- | --- |
| **Primary object** | `primary_object` | The object the record was created as. This is the record's home type. |
| **Morph objects** | `morph_objects` | Additional object contexts where the record should also appear. Semicolon-separated object names, e.g. `contact;candidate`. |
| **All objects** | Derived | Primary object + morph objects. Used for validation and unique checks. |


Important rules:

- The **primary object is never a morph object**.
- Morph objects are **additional visibility contexts**, not separate records.
- A morphed record still has **one UUID** and **one value set**.


When a record is morphed, Caraer makes it available in every object listed in
**all objects**. That is why the same record appears when you list contacts,
candidates, or persons — you are querying different object views of the same
underlying data.

Example record after morphing:


```json
{
  "uuid": "RECORD_UUID",
  "primary_object": "person",
  "morph_objects": "contact;candidate",
  "properties": {
    "firstname": "Alex",
    "email": "alex@example.com"
  }
}
```

## How morphing works

When a record is saved or morphed, Caraer:

1. Resolves the record's primary object and morph objects.
2. Applies default morph objects from the primary object's `extendsTo`
configuration (see below).
3. Validates morph rules (no duplicate primary, trait limits).
4. Persists `primary_object` and `morph_objects` on the record.


On create and update, morphing runs automatically as part of the save flow.
You can also morph an existing record explicitly through the API.

## Object-level configuration: `extendsTo`

Objects can define default morph targets through **`extendsTo`**. When
`person` has `extendsTo: [contact, candidate]`:

- A record created with primary object `person` automatically gets
`contact` and `candidate` as morph objects.
- You do not need to call the morph endpoint for every new person record.


`extendsTo` is schema configuration on the **object**, not per-record data.
Use it when a primary type should always be visible in other object contexts.

### Syncing existing records after schema changes

If you add or change `extendsTo` on an object, existing records are not
updated automatically. Call:

- `POST /api/v2/objects/{uuid}/syncMorphObjects`


This re-applies morph configuration for existing records that belong to that
object.

## Example: person → contact and candidate

### Scenario

You want:

- `person` to hold shared identity fields (`firstname`, `lastname`, `email`)
- `candidate` to expose recruitment-specific fields and views
- `contact` to expose CRM-specific fields and views
- One underlying record to appear in all relevant lists


### Recommended setup

1. Create three objects: `person`, `candidate`, `contact`.
2. Attach shared properties (`firstname`, `lastname`, `email`) to all three
objects, or at least to `person` and the specialized types.
3. Configure `person.extendsTo = [contact, candidate]`.
4. Create records with primary object `person`.


Result for a new person record:


```json
{
  "primary_object": "person",
  "morph_objects": "contact;candidate"
}
```

The record is visible in person, contact, and candidate list views.

### Querying the same record in different contexts

Use `mainObject` to choose which object view you are listing:


```bash
# Same record appears here because it is morphed into candidate
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "candidate"
  }'
```


```bash
# Same UUID, different object context
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "contact"
  }'
```


```bash
# List all persons, including those morphed into other types
curl -X POST "https://your-company.caraer.com/api/v2/records/index" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "page": 1,
    "limit": 25,
    "mainObject": "person"
  }'
```

The property values are identical across contexts. What changes is which object
schema, views, and permissions apply for that request.

### Alternative: morph per record

If only some people should also be contacts or candidates, skip `extendsTo` and
morph records individually:


```bash
curl -X POST "https://your-company.caraer.com/api/v2/records/RECORD_UUID/morph" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "objects": [
      { "name": "contact" },
      { "name": "candidate" }
    ]
  }'
```

The morph endpoint sets morph objects for that record. It does not change the
record's primary object.

## Morph API reference

### Morph a record

`POST /api/v2/records/{uuid}/morph`

Request body:


```json
{
  "objects": [
    { "name": "contact" },
    { "name": "candidate" }
  ]
}
```

Use this to:

- Add visibility in additional object contexts
- Remove visibility by sending a smaller object list (objects not in the list
are removed from the morph set)


Query params:

- `recordReturnFormat` — `LEGACY`, `USER_FRIENDLY`, or `EXPANDED`
- `parse` — parse formatted values before returning the record


### Sync morph objects for an object

`POST /api/v2/objects/{uuid}/syncMorphObjects`

Use after changing an object's `extendsTo` configuration so existing records
pick up the new default morph targets.

## Validation and constraints

Caraer enforces these morph rules:

- **Primary object cannot appear in morph objects.** If `person` is primary,
do not include `person` in the morph list.
- **At most one Page trait** across primary + morph objects.
- **At most one User trait** across primary + morph objects.


These limits prevent conflicting trait behavior when one record spans multiple
object types.

Unique-property checks consider **all objects** on the record. If `email` is
unique on both `person` and `contact`, duplicate detection spans the combined
object context.

## Design guidelines

- Use a **base primary object** (`person`) when multiple specialized types
share the same identity data.
- Use **`extendsTo`** for defaults that should apply to every new record of
that type.
- Use the **morph endpoint** for exceptions and lifecycle changes (for example,
promoting one person to candidate only).
- Keep shared property names aligned across morphed objects so values stay
consistent in every view.
- After schema changes to `extendsTo`, run **`syncMorphObjects`**.
- Query with the object context you need via **`mainObject`**; do not assume
records only exist under their primary object.


## Common pitfalls

| Pitfall | What happens | Fix |
|  --- | --- | --- |
| Expecting morph to change primary object | Primary object stays the same | Create the record under the intended primary type, or accept that `primary_object` reflects creation context |
| Forgetting `extendsTo` only affects new saves + sync | Existing records keep old morph set | Call `syncMorphObjects` after schema changes |
| Including primary object in morph request | Validation error | Remove the primary object from the morph list |
| Different property sets per object | Record may lack expected fields in one view | Attach required properties to every object context users will open |


## Related reading

- [Caraer data model: Object, Property, Relation](/blog/2026-03-25-caraer-data-model-object-property-relation)
- [Create and share properties](/blog/2026-06-07-create-and-share-properties)
- [Save records](/blog/2026-03-25-save-records)
- [Fetch records and filtering guide](/blog/2026-03-25-fetch-records-and-filtering-guide)