Skip to content

fix: partial descriptor behavior - #3215

Open
inemtsev wants to merge 2 commits into
Kotlin:devfrom
inemtsev:descriptor-hashcode-fix
Open

fix: partial descriptor behavior#3215
inemtsev wants to merge 2 commits into
Kotlin:devfrom
inemtsev:descriptor-hashcode-fix

Conversation

@inemtsev

@inemtsev inemtsev commented Jul 4, 2026

Copy link
Copy Markdown

Fixes #1512

Problem

PluginGeneratedSerialDescriptor instances created without a generated serializer throw
ArrayIndexOutOfBoundsException from hashCode(), equals() and toString(). This happens
for serializers produced by the @Serializer(forClass = ...) companion shortcut:

@Serializable(Model.Companion::class)
data class Model(val a: Int, val b: String) {
    @Serializer(forClass = Model::class)
    companion object
}

For these, the plugin constructs the descriptor with generatedSerializer = null, so
childSerializers is empty while elementsCount > 0 and all three identity methods walk
getElementDescriptor(i), which indexes into the empty array.

The bug is invisible on ordinary encode/decode (nothing calls hashCode), but it breaks any
feature that uses descriptors as cache keys in DescriptorSchemaCache. This is exactly the
crash reported in #1512 (open since 2021). The stack trace there is this path verbatim:
DescriptorSchemaCache.get -> PluginGeneratedSerialDescriptor.hashCode -> AIOOBE. The
originally reported trigger is useAlternativeNames = true (the documented limitation:
JsonCustomSerializersTest must run with useAlternativeNames = false), and a later report
in that thread hits the same crash via coerceInputValues = true. Because the root cause is
descriptor identity itself, every schema-cache entry point is affected the same way: in #3189
it forced a try-catch around the schema-cache lookup, and any future descriptor-keyed cache
would inherit the same landmine.

Fix

Identity of such "incomplete" descriptors is now computed from data the descriptor itself
owns: serial name, type parameters, kind, and element names, instead of the child descriptors
it cannot provide:

  • Complete descriptors are untouched: same hash values, same equality semantics as before.
  • Incomplete descriptors hash/compare by serialName + typeParams + kind + element names.
  • Complete vs incomplete never compare equal (previously threw), which is what keeps the
    equals/hashCode contract intact with two different hash formulas. The kind check also
    prevents accidental equality with EnumDescriptor, which reuses the null-serializer
    construction but overrides hashCode itself.
  • toString() for incomplete descriptors prints element names without child type names.

The new branch executes only inside the existing lazy _hashCode initializer and in equals,
so there is no hot-path cost (verified: TwitterFeedBenchmark decode/encode at parity with
master).

Benchmark master (ops/s) this PR (ops/s) Δ
decodeTwitter 1591 ± 22 1596 ± 38 +0.3%, intervals overlap (parity)
encodeTwitter 3244 ± 12 3200 ± 54 -1.4%, intervals overlap (parity)

@inemtsev

inemtsev commented Jul 4, 2026

Copy link
Copy Markdown
Author

Hi @sandwwraith @fzhinkin, please take a look at this improvement.

It fixes a crash path (that was commented for @JsonNames annotation in the past), but also allows me to clean up try/catch in #3189

Would be great to apply this fix first before cleaning up the above PR.

@inemtsev inemtsev changed the title fix-partial-descriptor-behavior fix: partial descriptor behavior Jul 4, 2026
@inemtsev
inemtsev changed the base branch from master to dev July 4, 2026 13:52
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