medusajs/medusa · error · MedusaError

Many-to-one relation ${relation.name} must be set with an ID

Error message

Many-to-one relation ${relation.name} must be set with an ID

What it means

Thrown while building the write payload in mikro-orm-repository.ts: a many-to-one relation was supplied as a nested object without an `id` property (and no other ID source), and Medusa does not support creating the related entity inline during this upsert. Many-to-one relations must be set by referencing an existing ID.

Source

Thrown at packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts:1243

      // If it is a many-to-one we ensure the ID is set for when we want to set/unset an association
      if (relation.kind === ReferenceKind.MANY_TO_ONE) {
        if (originalData === null) {
          entryCopy[relation.joinColumns[0]] = null
          return null
        }

        // The relation can either be a primitive or the entity object, depending on how it's defined on the model
        let relationId
        if (isString(originalData)) {
          relationId = originalData
        } else if ("id" in originalData) {
          relationId = originalData.id
        }

        // We don't support creating many-to-one relations, so we want to throw if someone doesn't pass the ID
        if (!relationId) {
          throw new MedusaError(
            MedusaError.Types.INVALID_DATA,
            `Many-to-one relation ${relation.name} must be set with an ID`
          )
        }

        entryCopy[relation.joinColumns[0]] = relationId
        return originalData
      }

      return undefined
    }

    // Returns a POJO object with the ID populated from the entity model hooks
    protected getEntityWithId(
      manager: SqlEntityManager,
      entityName: string,
      data: any
    ): Record<string, any> & { id: string } {

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Change the nested object to reference an existing entity: { relation: { id: 'existing_id' } } or set the FK field directly (relation_id).
  2. If the related entity does not exist yet, create it first with its own service call, then pass its returned ID in the upsert.
  3. If the field is actually a one-to-many/many-to-many managed relation, restructure the payload so the parent owns the array of children rather than the reverse.

Example fix

// before
await pricingService.upsert([
  { price_set: { name: 'default' } }, // many-to-one without id
])

// after
const set = await pricingService.createPriceSets([{ name: 'default' }])
await pricingService.upsert([
  { price_set: { id: set[0].id } }, // set with an ID
])
Defensive patterns

Strategy: validation

Validate before calling

for (const item of payload) {
  for (const [key, value] of Object.entries(item)) {
    if (value && typeof value === 'object' && !('id' in value) && isManyToOne(key)) {
      throw new Error(`Relation ${key} must reference an existing id`)
    }
  }
}

Type guard

function hasId(v: unknown): v is { id: string } {
  return typeof v === 'object' && v !== null && typeof (v as any).id === 'string' && (v as any).id.length > 0
}

Prevention

When it happens

Trigger: Upserting with payload { variant: { title: 'x', options: {...} } } where `variant` is many-to-one and the object carries no id; passing a partially-built object (e.g. from a form) instead of { id: 'variant_123' }; passing an object whose id field is undefined after destructuring.

Common situations: Admin/API payloads that embed full nested objects instead of IDs; assuming create-or-connect semantics (upsertWithReplace does not create many-to-one targets); data transformations that strip or rename the id key; migrating from a flow that previously created the child entity separately.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/69268d4107deaa28. Report an issue: GitHub.