Skip to content

Kotlin2 equals + hashcode + toString - #951

Open
bugy wants to merge 3 commits into
Netflix:masterfrom
bugy:kotlin2-equals-hashcode
Open

Kotlin2 equals + hashcode + toString#951
bugy wants to merge 3 commits into
Netflix:masterfrom
bugy:kotlin2-equals-hashcode

Conversation

@bugy

@bugy bugy commented Aug 3, 2026

Copy link
Copy Markdown

Hi all, I noticed that new kotlin generator with projections and nullable field types do not support lazy equals + hashcode + toString.
The latter (toString) I would like to bring here for sure (it would be more or less safe and doesn't affect how people use DTOs). There is an open issue for it: #638
equals+hashCode I would like to have too, to have it consistent with other DGS generated files.
Proposed implementation of equals+hashCode compares only initialized fields. The fields which are not initialized are considered equal.

cc @mbossenbroek what do you think?

yshepilov added 3 commits August 3, 2026 10:12
kotlin2 response/data types (generateKotlinNullableClasses = true) had no
equals/hashCode and compared by identity, since their fields are backed by
lazy supplier lambdas that throw when a field wasn't requested. Add
generated equals/hashCode where two instances are equal iff they requested
the same set of fields and every requested field's value is equal; an
unrequested field's supplier is never invoked. This mirrors the semantics
and CodeBlock style of the existing Java DataTypeGenerator.addEquals/
addHashcode.

Regenerates all kotlin2 integration-test golden expected files.
Renders only the fields the projection actually requested, so an unrequested
field's supplier is never invoked. Addresses Netflix#638.
Same semantics as before (same-class check, identical requested-field set
required, never invoke an unrequested supplier, absent != requested-null,
never throws) - purely a shape change, requested by review because the
previous joinToCode-based bodies wrapped badly.

- equals/hashCode now delegate to a private `__$fieldValues(): List<Any?>`
  helper that snapshots every field (using the field's own default lambda
  as the "unrequested" placeholder instead of invoking it). Two instances
  are equal iff `this === other || (other is T && fieldValues() ==
  other.fieldValues())`; hashCode is `fieldValues().hashCode()`. A
  mismatched requested-field set already forces a mismatched list entry
  at that field's index, so no separate "same requested set" check is
  needed on top - verified by the existing runtime test, left unmodified.
- toString gets its own `__$fieldStrings(): List<String>` helper (it needs
  field names, so it can't reuse fieldValues()), then joins them.
- Both helper names are backtick-escaped and contain "$", which GraphQL
  Names can never contain, so they can't collide with a member derived
  from any field (e.g. a field literally named `fieldValues`).
- The list literals are built from plain "\n"-containing string literals,
  not KotlinPoet's indent()/unindent(), which - confirmed by reproducing
  it in isolation - leaves the CodeWriter's indent level unbalanced after
  a single-expression function body collapses to `= ...`.

Regenerates 97 golden expected-output files (the one zero-field case's
toString was already just "ClassName()" and is unchanged).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant