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
- Implement __toString() on the custom value object used as a key.
- If the object is a BackedEnum, ensure enum_value() can resolve it (it does for UnitEnum).
- Adjust the custom cast's get() to return a scalar string/int instead of an object for key columns.
- 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
- Implement __toString on any custom value object used as a key/cast output.
- Have key casts return scalars (string/int) rather than objects.
- Pass scalar IDs to attach()/sync() rather than object keys.
- For BackedEnum keys, ensure you are on PHP 8.1+ so UnitEnum handling applies.
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
- Call to undefined method
- Collection given to whereAttachedTo method may not be empty.
- Collection given to whereBelongsTo method may not be empty.
- Invalid aggregate [ ] used within ofMany relation…
- The binary codec format is required.
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)