An array cast decodes on read and encodes on write, and modifying the decoded array in place does not mark the attribute as dirty — so the save writes nothing.
// does not save
$order->options['gift_wrap'] = true;
$order->save();
// works
$options = $order->options;
$options['gift_wrap'] = true;
$order->options = $options;
$order->save();
// or, on a JSON column, let MySQL do it
DB::table('orders')->where('id', $id)
->update(['options->gift_wrap' => true]);
The first version does not even produce an error — it modifies a temporary array returned by the accessor and discards it. That silence is what makes this a recurring bug rather than a one-time lesson. The arrow syntax in the query builder writes into the JSON document server-side, which avoids reading and rewriting the whole column and is safe against a concurrent update to a different key.