laravel/framework · error · InvalidArgumentException

Model attribute value is an object but does not have a…

Error message

Model attribute value is an object but does not have a __toString method.

What it means

getDictionaryKey() converts relationship keys (foreign keys, owner keys, morph maps) into a usable scalar for the internal dictionary lookup. When it receives an object that is neither a string-castable value (__toString), nor a UnitEnum (handled by enum_value), it throws, because there is no safe way to use that object as a dictionary key.

Solutions

  1. Implement __toString() on the custom value object used as a key.
  2. If the object is a BackedEnum, ensure enum_value() can resolve it (it does for UnitEnum).
  3. Adjust the custom cast's get() to return a scalar string/int instead of an object for key columns.
  4. Pass scalar IDs to attach()/sync() instead of object keys.

Example fix

// before - value object as a key column
class Uuid { public function __construct(public string $value) {} }
class User extends Model {
    protected $casts = ['id' => UuidCast::class]; // returns Uuid object
}
$user->roles()->attach($roleId); // throws

// after
class Uuid {
    public function __construct(public string $value) {}
    public function __toString(): string { return $this->value; }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure key values used by relationships are scalar/stringable
$key = $model->getKey();
if (is_object($key) && ! method_exists($key, '__toString') && ! ($key instanceof \UnitEnum)) {
    throw new \RuntimeException('Model key is an object without __toString; relationship lookups will fail.');
}

Type guard

function isUsableDictionaryKey(mixed $value): bool {
    if ($value === null || is_string($value) || is_int($value)) return true;
    if (is_object($value)) {
        return method_exists($value, '__toString') || $value instanceof \UnitEnum;
    }
    return true;
}

Try / catch

try {
    $model->roles()->attach($roleId);
} catch (\InvalidArgumentException $e) {
    if (str_contains($e->getMessage(), 'does not have a __toString method')) {
        $roleId = is_object($roleId) ? (string) $roleId : $roleId; // coerce after fixing the VO
    } else { throw $e; }
}

Prevention

When it happens

Trigger: A model's primary/foreign key attribute resolving to a plain object (e.g. a value object without __toString) when a relationship tries to build its dictionary - common with custom cast classes that return objects, or with attach()/sync() receiving an object as a key.

Common situations: Custom key casts returning value objects (UUID/Money/Decimal objects) without __toString; attaching a model whose getKey() returns an object; enums used as keys on PHP < 8.1 paths; morph map values that are objects.

Related errors


AI-assisted analysis of laravel/framework@e0f6eb3518 (2026-08-11). Data as JSON: /api/errors/441115bb3b2dc1f7. Report an issue: GitHub.

Appendix: source

Thrown at src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithDictionary.php:35

     *
     * @throws \InvalidArgumentException
     */
    protected function getDictionaryKey($attribute)
    {
        if (is_null($attribute) || is_string($attribute) || is_int($attribute)) {
            return $attribute;
        }

        if (is_object($attribute)) {
            if (method_exists($attribute, '__toString')) {
                return $attribute->__toString();
            }

            if ($attribute instanceof UnitEnum) {
                return enum_value($attribute);
            }

            throw new InvalidArgumentException('Model attribute value is an object but does not have a __toString method.');
        }

        return (string) $attribute;
    }
}

View on GitHub (pinned to e0f6eb3518)