Adereços loucos para @Dave Redfern no Slack, que apontou meu problema. Ao passar um array indexado diferente de zero, ele é interpretado como um objeto.
dump(json_encode([
0 => "ROLE_SITE_DIRECTOR", 2 => "ROLE_TRANSLATOR", 1 => "ROLE_DATA_ENTRY",
]));
dump(json_encode(array_values([
0 => "ROLE_SITE_DIRECTOR", 2 => "ROLE_TRANSLATOR", 1 => "ROLE_DATA_ENTRY",
])));
saída será:
"{"0":"ROLE_SITE_DIRECTOR","2":"ROLE_TRANSLATOR","1":"ROLE_DATA_ENTRY"}"
"["ROLE_SITE_DIRECTOR","ROLE_TRANSLATOR","ROLE_DATA_ENTRY"]"
Então a correção é simples, sou meu setter:
$this->roles = array_values($roles);
Seguindo em frente, também é uma prática melhor usar relacionamentos de banco de dados para as funções. Pesquisar por dados JSON não é divertido, mas em uma junção é uma prática padrão.