Skip to content

fix(java): preserve and escape all-underscore discriminator values - #17432

Open
ChoMinGi wants to merge 3 commits into
fern-api:mainfrom
ChoMinGi:fix/java-underscore-discriminator
Open

ChoMinGi wants to merge 3 commits into
fern-api:mainfrom
ChoMinGi:fix/java-underscore-discriminator

Conversation

@ChoMinGi

@ChoMinGi ChoMinGi commented Aug 17, 2026 •

Copy link
Copy Markdown

Description

Closes #17431

CasingConfiguration.computeName() builds Java identifiers via toCamelCase(), whose word-splitting regex only recognizes letter/digit runs. Any discriminator value with no letters or digits at all (e.g. "_", "-", "@", "-_-") therefore collapsed to "" before the reserved-keyword check ever ran, so JavaPoet rejected the resulting empty method name (IllegalArgumentException: not a valid name: ), aborting generation.

This is a different root cause from #17430 (missing null/true/false entries in the same keyword set) — here the keyword list is never even reached, since the input is discarded earlier in casing conversion.

The fix went through two iterations, both included in this PR:

  1. Initially special-cased all-underscore input only. A reviewer correctly pointed out this was too narrow — any wordless input hits the same bug.
  2. Generalized to strip illegal characters and keep whatever's left, falling back to _ when nothing legal remains. Verifying this end-to-end (a union with two wordless variants, "-" and "@", as siblings) surfaced a follow-on bug: distinct wordless values all fell back to the same identifier, so generation "succeeded" but silently emitted non-compiling Java (duplicate method signatures, a duplicate nested class name). Fixed by encoding the stripped characters' code points instead of collapsing to a shared placeholder, so distinct inputs never collide.

Changes Made

  • CasingConfiguration.computeNameInternal(): when casing produces an empty name, fall back to legal Java-identifier characters stripped from the input, or an encoding of the input's code points (e.g. - → __2d, @ → __40) if none remain, so distinct wordless discriminants never collide
  • Added "_" to JAVA_RESERVED_KEYWORDS so a preserved "_" gets escaped to "__", consistent with how other reserved words are handled
  • Added tests covering _/__/___ (escaped/already-valid), ordinary underscore-separated names (unaffected), wordless symbols (-, @, -_-, $$), and a non-collision assertion between distinct wordless inputs
  • Added a changelog entry under generators/java/sdk/changes/unreleased/

Testing

CasingConfiguration.computeName() built Java identifiers via toCamelCase(),
which uses a word-splitting regex that never matches underscores. An
all-underscore input like "_" therefore collapsed to an empty string before
the reserved-keyword check ever ran, so JavaPoet rejected the resulting
empty method name for a union discriminant of "_".

Short-circuit all-underscore inputs before word-splitting so they keep
their literal underscores, and add "_" to JAVA_RESERVED_KEYWORDS so it
escapes to "__" like any other reserved identifier, while the serialized
discriminator value stays "_".

Fixes fern-api#17431

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Narrow, targeted fix: short-circuits all-underscore names before word-splitting and adds _ to the Java keyword set. Logic looks correct for the reported case, but the underlying failure mode (casing collapsing input to "") is broader than underscores, and the new tests only assert the camel variants of a 9-positional-arg NameParts.

  • 🔵 3 suggestion(s)

Comment on lines +175 to +178
if (ALL_UNDERSCORES.matcher(name).matches()) {
String safeName = sanitizeName(name);
return new NameParts(inputName, name, safeName, name, safeName, name, safeName, name, safeName);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

This fixes underscores specifically, but any input whose characters don't match SPLIT_WORDS_PATTERN collapses to "" and will blow up JavaPoet the same way (e.g. discriminator values "-", "@", "$$", "-_-"). Consider a general fallback after casing: if the computed camel/pascal name is empty, fall back to the preprocessed input (or a sanitized placeholder) rather than special-casing ^_+$. Same class of bug, one guard.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — you were right, this was too narrow. Generalized it to strip illegal-for-Java-identifier characters and keep whatever's left, falling back to _ (escaped to __) only if nothing legal remains.

Verifying that end-to-end (union with two wordless variants, - and @, as siblings) actually surfaced a worse follow-on bug your suggestion's exact approach would've hit too: both fell back to the same placeholder, so generation reported success but silently emitted non-compiling Java (duplicate is_() method, duplicate __Value nested class). Fixed by encoding the stripped characters' code points instead (- -> __2d, @ -> __40) so distinct inputs can't collide. Details in the updated PR description.


if (ALL_UNDERSCORES.matcher(name).matches()) {
String safeName = sanitizeName(name);
return new NameParts(inputName, name, safeName, name, safeName, name, safeName, name, safeName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

Nine positional args alternating name/safeName is easy to get wrong (or to silently mis-order if the record's field order ever changes) and the new tests only assert camelUnsafe/camelSafe, so a swap would go unnoticed. Worth asserting pascal/snake/screamingSnake variants in the tests too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added assertions on pascalSafe/snakeSafe/screamingSnakeSafe (not just camelSafe) in the new WordlessNameTests, so a mis-ordered NameParts arg would now get caught.

rejected the resulting empty method name. All-underscore inputs are now kept
intact, and `_` is escaped to `__` (matching the existing convention used for
other reserved words), while the serialized discriminator value is unchanged.
type: fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

The fix lives in generator-utils, which the model (and other Java generators) also consume. If those generators ship their own changelogs, consider adding matching entries so consumers of fern-java-model etc. see the fix too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked — model doesn't have the changes/unreleased/ changelog automation that sdk does (no directory, no template), so there's nowhere equivalent to add an entry there right now. Scoping the changelog to sdk to match what's actually been verified end-to-end.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

The previous commit only special-cased all-underscore input. Any
discriminator value with no letters or digits (e.g. "-", "@", "-_-")
hits the same splitWords()-collapses-to-empty-string bug and crashes
JavaPoet the same way, since word-splitting never recognizes those
characters as words either.

Generalize the fallback: when casing produces an empty name, strip
characters that aren't legal in a Java identifier and keep whatever's
left ("$$" stays "$$", "-_-" becomes "_"), falling back to "_" (then
escaped to "__") only if nothing legal remains.
The wordless-name fallback added in the previous commit collapsed any
input with no legal Java-identifier characters left (e.g. "-", "@") to
the same placeholder ("_"). Two such discriminants as siblings in one
union therefore generated identically-named methods and a duplicate
nested class - generation reported success but emitted non-compiling
Java.

Encode the stripped characters' code points instead of collapsing to a
shared placeholder, so distinct inputs always produce distinct
identifiers (e.g. "-" -> "__2d", "@" -> "__40").
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 25 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@github-actions github-actions Bot added the Stale This PR hasn't has any commits or comments in 25 days or more. label Sep 12, 2026
@ChoMinGi

Copy link
Copy Markdown
Author

@iamnamananand996 Related to #17430, this fixes a separate casing failure for wordless discriminator values such as _, -, and @, including collisions between fallback identifiers. Could you also take a look at this PR?

@github-actions github-actions Bot removed the Stale This PR hasn't has any commits or comments in 25 days or more. label Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open 25 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@github-actions github-actions Bot added the Stale This PR hasn't has any commits or comments in 25 days or more. label Oct 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stale This PR hasn't has any commits or comments in 25 days or more.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Java SDK generation fails on discriminator value _

1 participant