[Elasticsearch] Documented snake_case → camelCase document field conversion never happens
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 55/100
调研方向
从 src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php、src/Symfony/Bundle/Resources/config/elasticsearch.php 和 src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php 开始,追踪注入了哪个名称转换器。使用一个 snake_case 的 Elasticsearch 字段复现,并对照 tests/Functional/Elasticsearch/ReadTest.php 和 tests/Fixtures/Elasticsearch/Fixtures/user.json。完成的标准是:文档中说明的 snake_case 到 camelCase 的行为已被覆盖且能够正常工作,或者矛盾的文档/默认值已被移除。
由索引模型根据 Issue 内容生成。
描述
API Platform version(s) affected: 4.3.17 (4.x since MetadataAwareNameConverterPass was introduced)
Description
The Elasticsearch documentation states:
API Platform will automatically disable write operations and snake_case document fields will automatically be converted to camelCase object properties during serialization.
This conversion never happens. A content_key document field does not populate a $contentKey property — the property keeps its default value, silently.
InnerFieldsNameConverter does implement the documented behaviour, as its constructor default:
// src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php
public function __construct(private readonly NameConverterInterface $inner = new CamelCaseToSnakeCaseNameConverter())
but that default is unreachable. The service definition passes an argument that is always valid:
// src/Symfony/Bundle/Resources/config/elasticsearch.php
$services->set('api_platform.elasticsearch.name_converter.inner_fields', InnerFieldsNameConverter::class)
->args([service('api_platform.name_converter')->ignoreOnInvalid()]);
ignoreOnInvalid() would drop the argument — and let the CamelCaseToSnakeCaseNameConverter default apply — only if api_platform.name_converter did not exist. But MetadataAwareNameConverterPass defines and aliases it unconditionally, whether or not the user configured api_platform.name_converter:
// src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php
$container->setDefinition('api_platform.name_converter.metadata_aware', $definition);
$container->setAlias('api_platform.name_converter', 'api_platform.name_converter.metadata_aware');
Its only early return is !$container->hasDefinition('serializer.mapping.class_metadata_factory'), which never holds in a Symfony app with the serializer enabled.
Documents are therefore denormalized through MetadataAwareNameConverter, which is an identity converter for every property that has no #[SerializedName].
How to reproduce
Index content holds:
{ "_id": "abc", "_source": { "content_key": "my-key" } }
Resource:
#[ApiResource(stateOptions: new Options(index: 'content'))]
class Content
{
public string $id = '';
public string $contentKey = '';
}
GET /contents/abc returns "contentKey": "". Renaming the property to $content_key returns "my-key", confirming the document key is matched literally.
Reproduced directly against the service, without HTTP:
$hit = ['_id' => 'abc', '_source' => ['content_key' => 'my-key']];
$content = $container->get('api_platform.elasticsearch.normalizer.document')
->denormalize($hit, Content::class, DocumentNormalizer::FORMAT);
$content->contentKey; // '' — expected 'my-key'
api_platform.elasticsearch.name_converter.inner_fields receives HydraPrefixNameConverter → MetadataAwareNameConverter, and denormalize('content_key', Content::class, 'elasticsearch') returns 'content_key'.
Second trap: #[SerializedName] does not work around it
The obvious workaround fails when the property carries serialization groups, which is the common case:
#[Groups(['content:read'])]
#[SerializedName('content_key')]
public string $contentKey = '';
MetadataAwareNameConverter::getCacheValueForAttributesMetadata() skips any attribute whose metadata has groups when the context has none — and the Elasticsearch denormalization context carries no groups:
if ($metadataGroups && !array_intersect($metadataGroups, $contextGroups) && !\in_array('*', $contextGroups, true)) {
continue;
}
Normalization has no such group check, so the result is the worst of both: the JSON output field is renamed to content_key, while reading the document still fails.
Regarding #4053
#4053 reports this exact symptom and was closed as fixed, referencing tests/Functional/Elasticsearch/ReadTest.php. That test cannot catch it: the fixtures store camelCase document fields —
// tests/Fixtures/Elasticsearch/Fixtures/user.json
{ "firstName": "Kilian", "lastName": "Jornet", "registeredAt": "2009-09-01" }
— so it covers multi-word camelCase fields, not snake_case documents. Worth reopening, or covering with a fixture using snake_case keys.
Possible Solution
Either behaviour or documentation, depending on the intended convention:
- Restore the documented behaviour — inject
serializer.name_converter.camel_case_to_snake_caseintoapi_platform.elasticsearch.name_converter.inner_fieldsunless the user explicitly configuredapi_platform.name_converter(the compiler pass knows the difference:$container->hasAlias(...)before it sets its own alias), or expose a dedicatedapi_platform.elasticsearch.name_convertersetting. - Drop the claim — the same doc page also says "all fields should be lower case and should use camelCase for combining words", which contradicts the snake_case sentence and matches the fixtures. If camelCase documents are the intended convention, remove the sentence and the misleading
InnerFieldsNameConverterconstructor default.
Either way the current state is a silent data loss: a mismatched field yields an empty property, with no error.
Additional Context
Userland workaround, scoped to Elasticsearch so the API output stays camelCase:
services:
api_platform.elasticsearch.name_converter.inner_fields:
class: ApiPlatform\Elasticsearch\Serializer\NameConverter\InnerFieldsNameConverter
autowire: false
autoconfigure: false
arguments: ['@serializer.name_converter.camel_case_to_snake_case']
PHP 8.4, Symfony 8.1, symfony/serializer 8.1.4, api-platform/elasticsearch 4.3.17, Elasticsearch 8.
- 主要语言
- PHP
- 星标
- 2.6k
- 派生
- 989
- 平均合并
- 1 天 22 小时
- 30 天内合并 PR
- 100
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
api-platform/core 的其他 Issue
-
JSON:API: resource schemas (`*.jsonapi`) have `properties` but no `type: object`可能已有人在做 @ousamabenyounes 于 2 天前认领。 未关闭
难度 2/5 1 小时以内 新手友好度 80/100
api-platform/core#8701 ·
维护者通常 1 天内回复
-
JSON:API: `JsonApiCollectionBaseSchemaNoPagination` declares `data` as `type: array` without `items`未关闭
难度 2/5 1-3 小时 新手友好度 82/100
api-platform/core#8699 ·
维护者通常 1 天内回复
-
[Json-Schema] Nested resources lost externalDoc and description data可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 3/5 1-2 天 新手友好度 68/100
api-platform/core#8721 ·
维护者通常 1 天内回复
-
难度 3/5 1-2 天 新手友好度 50/100
api-platform/core#8704 ·
维护者通常 1 天内回复
-
难度 3/5 1-2 天 新手友好度 62/100
api-platform/core#8703 ·
维护者通常 1 天内回复
查看 api-platform/core 的全部 Issue
相似的 Issue
-
domain/discovery
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
neos/flow-development-collection#3639 · 1 条评论 ·
维护者通常 1 天内回复
-
The PHP version check in prepare.php still allows PHP 7.2 and 7.3可能已有人在做 @ekamran 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 72/100
WordPress/phpunit-test-runner#359 ·
维护者通常 2 天内回复
-
[ObjectMapper] Merge the objects that repeated `#[Map]` attributes map …可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭ObjectMapper
难度 2/5 1-3 小时 新手友好度 68/100
symfony/symfony-docs#23390 ·
维护者通常 2 天内回复