-
Notifications
You must be signed in to change notification settings - Fork 13
Naming Contract
retrofit-graphql's codegen plugin follows a strict naming contract with distinct Kotlin identifiers and GraphQL wire names. This page documents the contract.
Each generated identifier carries these representations stored in GeneratedName:
| Name | Purpose | Example |
|---|---|---|
kotlinName |
Identifier used in generated Kotlin source code | privateValue |
wireName |
Original GraphQL name for serialization annotations | "private" |
// wireName is ALWAYS the original GraphQL name. Never derived from kotlinName.
val name = GraphNameAllocator().allocatePropertyName("private")
name.wireName == "private" // always true
name.kotlinName == "privateValue" // keyword-escaped for KotlinThis invariant guarantees that @SerialName("private") always produces the correct JSON key matching the GraphQL schema, regardless of Kotlin keyword escaping or collision resolution.
Kotlin hard keywords cannot be used as identifiers in any context. When a GraphQL name matches a hard keyword, the Value suffix is appended.
Examples (see NamePolicy.kt for the full set of 26 hard keywords):
| GraphQL Name | Kotlin Name | @SerialName |
|---|---|---|
as |
asValue |
"as" |
class |
classValue |
"class" |
false |
falseValue |
"false" |
fun |
funValue |
"fun" |
if |
ifValue |
"if" |
in |
inValue |
"in" |
interface |
interfaceValue |
"interface" |
is |
isValue |
"is" |
null |
nullValue |
"null" |
object |
objectValue |
"object" |
package |
packageValue |
"package" |
return |
returnValue |
"return" |
super |
superValue |
"super" |
this |
thisValue |
"this" |
throw |
throwValue |
"throw" |
true |
trueValue |
"true" |
try |
tryValue |
"try" |
typealias |
typealiasValue |
"typealias" |
val |
valValue |
"val" |
var |
varValue |
"var" |
when |
whenValue |
"when" |
while |
whileValue |
"while" |
Visibility modifiers are valid identifiers in some contexts but cause compilation ambiguity alongside actual visibility declarations. They are escaped:
| GraphQL Name | Kotlin Name | @SerialName |
|---|---|---|
private |
privateValue |
"private" |
protected |
protectedValue |
"protected" |
public |
publicValue |
"public" |
internal |
internalValue |
"internal" |
Names starting with a digit are prefixed with _:
| GraphQL Name | Kotlin Name | @SerialName |
|---|---|---|
3dModel |
_3dModel |
"3dModel" |
123 |
_123 |
"123" |
Other soft keywords are not escaped because they are valid identifiers in most generated contexts (property names, parameter names, class names):
-
actual,data,inner,sealed,open,override,operator, etc.
If a consumer encounters a problem with a specific soft keyword in a specific context, file an issue and the policy will be extended.
lowerCamelCase. The escaped name's first character is lowercased:
GraphQL "myField" -> kotlinName="myField", wireName="myField"
GraphQL "MyField" -> kotlinName="myField", wireName="MyField"
GraphQL "private" -> kotlinName="privateValue", wireName="private"
PascalCase. The escaped name's first character is uppercased:
GraphQL "GetCurrentUserData" -> kotlinName="GetCurrentUserData"
GraphQL "searchResult" -> kotlinName="SearchResult"
SCREAMING_SNAKE_CASE. The escaped name is uppercased in its entirety:
GraphQL "OPEN" -> kotlinName="OPEN", wireName="OPEN"
GraphQL "created_at" -> kotlinName="CREATED_AT", wireName="created_at"
GraphQL "private" -> kotlinName="PRIVATEVALUE", wireName="private"
When two distinct GraphQL names produce the same Kotlin candidate (after keyword escaping and case conversion), KotlinPoet's NameAllocator appends deterministic underscore-based suffixes:
Allocate "MyField" -> myField (first char lowercased)
Allocate "myField" -> myField_ (collision -- suffix added)
Allocate "myField2" -> myField2 (distinct -- no collision)
In this example, "MyField" and "myField" are distinct GraphQL names, but replaceFirstChar { it.lowercase() } in allocatePropertyName normalises both to "myField". The allocator resolves the collision by appending _ to the second candidate.
Each GraphNameAllocator instance is scoped (e.g., per class or per operation), so collisions are resolved within that scope only.
Generated response classes intentionally do not override their class-level kotlinx descriptor names. kotlinx.serialization therefore uses each generated class's default fully qualified name, while property annotations preserve the GraphQL wire names:
@Serializable
data class GetCurrentUserData(
@SerialName("viewer") val viewer: Viewer?,
) {
@Serializable
data class Viewer(
@SerialName("status") val status: Status?,
) {
@Serializable
data class Status(@SerialName("message") val message: String?)
}
}Concrete polymorphic subtypes still use class-level @SerialName values that
match their GraphQL __typename discriminator.
- Code Generation -- codegen DSL and naming convention reference