xlang: the snake_case conversion of field names is not specified, and the runtimes do not agree
Los mantenedores suelen responder en 1 día
@dhruv-dagar ya está trabajando en esto.
Desde el 19/10/2026.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 25/100
Línea de trabajo
Start with docs/specification/xlang_serialization_spec.md at the cited lines, then compare the conversion functions and cross-language tests named in the issue. The proposal requires choosing and specifying one exact conversion, applying it consistently to written names, sort order, and fingerprints across runtimes, and adding known-answer cases. Done also requires documenting how existing data with affected field names is handled.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Search before asking
- I had searched in the issues and found no similar issues.
Version
All
Component(s)
Other
Minimal reproduce step
Problem
The xlang specification says that field names are "converted to snake_case" (docs/specification/xlang_serialization_spec.md, lines 882 and 1732), and line 1800 refers to "the snake_case field name". It does not define the conversion. Each runtime has its own conversion, and the results are different for acronyms, an acronym followed by a digit, a leading capital letter, and non-ASCII capital letters. Rust and Python write the declared field name without a conversion.
Java, Kotlin, Scala, JS, Dart, C#, Swift, and C++ agree for lowerCamelCase names that have only single ASCII capital letters: they all write createdAt as created_at. A Go field must be exported, so Go writes created_at for CreatedAt. Rust and Python write the declared name, so they match only for a field declared as created_at.
Field name written to TypeDef/TypeMeta (apache/main at a11d257b4)
Every cell comes from a probe program that ran the runtime code at a11d257b4 and, where possible, encoded and decoded a real TypeDef/TypeMeta.
| Input | Java, Kotlin, Scala, JS | Go | Dart | C#, Swift | C++ | Rust | Python |
|---|---|---|---|---|---|---|---|
createdAt |
created_at |
(1) | created_at |
created_at |
created_at |
createdAt (2) |
createdAt |
fooURL |
foo_u_r_l |
(1) | foo_u_r_l |
foo_url |
foo_url |
fooURL (2) |
fooURL |
URLValue |
_u_r_l_value |
u_r_l_value |
u_r_l_value |
url_value |
url_value |
URLValue (2) |
URLValue |
HTTP2Server |
_h_t_t_p2_server |
h_t_t_p2_server |
h_t_t_p2_server |
htt_p2_server |
http2_server |
HTTP2Server (2) |
HTTP2Server |
CreatedAt |
_created_at |
created_at |
created_at |
created_at |
created_at |
CreatedAt (2) |
CreatedAt |
x2Y |
x2_y |
(1) | x2_y |
x2_y |
x2_y |
x2Y (2) |
x2Y |
fooÖl |
fooÖl |
(1) | (3) | foo_öl |
fooÖl |
(4) | (4) |
already_snake |
already_snake |
(1) | already_snake |
already_snake |
already_snake |
already_snake |
already_snake |
- Go writes only exported fields, so a name that starts with a lowercase letter does not reach a Go TypeDef. The
SnakeCasehelper givescreated_at,foo_u_r_l,x2_y, andfooÖlfor these names. - The Rust runtime writes the raw name (probe of
TypeMeta::newwithFieldInfo::new), but#[derive(ForyStruct)]does not compile for a field name with an ASCII capital letter (error E0063, "missing field"). The derive converts identifiers to snake_case atobject/util.rs:410and:422. fooÖlis not a valid Dart identifier.- Rust and Python stop with "Unsupported character for LOWER_UPPER_DIGIT_SPECIAL encoding". Java writes the name as UTF-8.
Some runtimes use a second conversion for the field sort order and the struct fingerprint, and for some inputs it gives a different result from their written name:
| Input | JS sort and fingerprint | Rust sort and fingerprint | Python sort and fingerprint |
|---|---|---|---|
fooURL |
foo_url |
foo_url |
foo_url |
URLValue |
url_value |
url_value |
url_value |
HTTP2Server |
htt_p2_server |
htt_p2_server |
http2_server |
CreatedAt |
created_at |
created_at |
created_at |
fooÖl |
fooÖl |
fooÖl |
foo_öl |
In C#, Swift, and C++, the second conversion gave the same result as the written name for every input.
Conversion functions
- Java:
StringUtils.lowerCamelToLowerUnderscore(StringUtils.java:259), applied atTypeDefEncoder.java:277. The probe built an xlangTypeDef, wrote each field withTypeDefEncoder.writeFieldsInfo, and found the decoded names inTypeDef.getEncoded(). Kotlin and Scala use this fory-core path and were not probed separately. - JavaScript:
lowerCamelToLowerUnderscore(TypeMeta.ts:979) writes the name (TypeMeta.ts:917).toSnakeCase(TypeMeta.ts:1059) gives the sort key, the fingerprint, and the read match. The probe encodedTypeMeta.fromTypeInfo(...).toBytes()and decoded the names. - Go:
SnakeCase(go/fory/util.go:60), applied atfield_spec.go:427and written attype_def.go:509. Unexported fields are skipped attype_def.go:494. The probe ranbuildTypeDefanddecodeTypeDef. - Dart:
_toSnakeCase(fory_generator.dart:5739, called at:1984). The runtime writes the generated identifier unchanged (type_resolver.dart:1181). The probe ranbuild_runnerand readTypeDef.encoded. - C#: the generator's
ToSnakeCase(ForyModelGenerator.Analysis.cs:2803, called at:1490) and the runtime'sLowerCamelToLowerUnderscore(TypeMeta.cs:81, applied at:333). Both usechar.IsUpper, so they convert non-ASCII capital letters. The probe compiled a[ForyStruct]class with the source generator and round-trippedTypeMeta.EncodeandDecode. A type with bothcreatedAtandCreatedAtfails the build with FORY016 ("duplicate structural field identity 'created_at'"). - Swift:
lowerCamelToLowerUnderscore(TypeMeta.swift:1123, applied at:267). The probe compiledSources/Foryand round-trippedTypeMeta.encodeanddecode. The macro'stoSnakeCase(ForyObjectMacro.swift:3698) gives the sort order; the probe ran a verbatim copy of it, because the macro build needs swift-syntax. - C++:
to_snake_case(cpp/fory/util/string_util.h:287), applied attype_resolver.h:1183. The probe builtfory_serialization, ranFieldInfoBuilder::build(), and round-trippedTypeMeta::to_bytesandfrom_bytes. The sort order usesnormalize_field_name(type_resolver.cc:1540), which calls a second, file-localto_snake_case(type_resolver.cc:1479). - Rust: the derive passes the raw identifier to
FieldInfo::new_with_id(rust/fory-derive/src/object/field_codec.rs:201). The probe ranTypeMeta::newwithFieldInfo::newand decoded the result.to_snake_case(string_util.rs:110) gives the sort order and the fingerprint. - Python: the raw dataclass attribute (
meta/typedef.py:1302), written bytypedef_encoder.write_field_info(typedef_encoder.py:247), which the probe ran with a recording buffer._to_snake_case(struct.py:1183) gives the sort order and the fingerprint. The probe did not runbuild_field_infosor the fullencode_typedef, because pyfory does not import without its native extension.
Proposal
Define one exact conversion in the xlang specification, apply it in every runtime for the written name, the sort order, and the fingerprint, and add known-answer cases (for example, the names in the table above) to the cross-language tests. A change to the conversion changes the wire names of some fields, so the specification must also say how existing data with such names is handled.
Related
We found no existing issue for this problem. The row-format schema-evolution work in #3714 uses the Java field-name conversion for its wire names and documents the portable subset, ordinary lowerCamelCase names, until this issue is resolved.
What did you expect to see?
xlang schema should always produce the same normalized names
What did you see instead?
different languages produce different names for the same schema
Anything Else?
Issue diagnosed with Claude Opus 5.5
Are you willing to submit a PR?
- I'm willing to submit a PR!
- Lenguaje dominante
- Java
- Estrellas
- 4.6k
- Forks
- 470
- Merge medio
- 9 h 28 min
- PR fusionados (30 d)
- 33
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de apache/fory
-
[Python] Compatible xlang read silently drops fields when the peer writes camelCase/PascalCase namesAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
Los mantenedores suelen responder en 1 día
-
[Build] Add ASF license headers to config files that allow commentsQuizá libre de nuevo @Swatantra-66 la tomó hace 30 días y no hay ningún pull request abierto. Abiertoenhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
apache/fory#4124 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
[Rust] Add an option to use Rust native enumPosiblemente ocupada @BaldDemian la tomó hace 21 días. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
apache/fory#4177 · 1 comentario · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
Untyped read collapses a number literal that does not fit a doublePosiblemente ocupada @mohitjoshi-hey la tomó hace 23 días. Abiertobug
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
apache/fory#4170 · 3 comentarios · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
Registered factory cannot tell a field from the root of the callPosiblemente ocupada @mohitjoshi-hey la tomó hace 20 días. Abiertobug
Dificultad 4/5 1-2 días Aptitud para principiantes 48/100
apache/fory#4169 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de apache/fory
Issues similares
-
API `/api/dataverses/{identifier}/dataset/linkingDataverses?alreadyLinking=true` returns at most 1 collectionPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. AbiertoType: Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
javaoperatorsdk.client.oauth-token has no effect on the Kubernetes clientPosiblemente ocupada @RaphaelFakhri la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
-
MCP add-on: JSON responses with non-ASCII characters are truncated (wrong Content-Length)Posiblemente ocupada @dhanushbs10 la tomó hoy. Abiertoadd-on bug in:mcp
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Map.Coordinate does not implement proper equalsPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertovaadin-map
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
vaadin/flow-components#10586 ·
Los mantenedores suelen responder en 1 día
-
WeReadInstaller writes the launcher database on the main thread at handover (Activity pause timeout)Abiertobug good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100