Skip to content

Java SDK generation fails on discriminator value _ #17431

Description

@ChoMinGi

Java SDK generation fails on discriminator value _

When a discriminated union's discriminator value is a single underscore ("_"), the Java generator aborts with:

IllegalArgumentException: not a valid name: 

(empty identifier — note the trailing space, nothing after the colon)

Reproduction

openapi: 3.0.3
info:
  title: Underscore discriminator repro
  version: 1.0.0

paths:
  /filter:
    post:
      operationId: createFilter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Filter"
      responses:
        "204":
          description: OK

components:
  schemas:
    Filter:
      oneOf:
        - $ref: "#/components/schemas/StringFilter"
        - $ref: "#/components/schemas/UnderscoreFilter"
      discriminator:
        propertyName: type
        mapping:
          string: "#/components/schemas/StringFilter"
          "_": "#/components/schemas/UnderscoreFilter"

    StringFilter:
      type: object
      properties:
        type:
          type: string
          enum: ["string"]
        value:
          type: string
      required: [type, value]

    UnderscoreFilter:
      type: object
      properties:
        type:
          type: string
          enum: ["_"]
        field:
          type: string
      required: [type, field]

Running fern ir confirms the IR correctly preserves "discriminantValue": "_".

Running fern generate --local against fernapi/fern-java-sdk:4.19.0 fails with:

Exception in thread "main" java.lang.RuntimeException:
java.lang.IllegalArgumentException: not a valid name: 
    at com.fern.java.AbstractGeneratorCli.run(AbstractGeneratorCli.java:412)
    at com.fern.java.client.Cli.main(Cli.java:122)
Caused by: java.lang.IllegalArgumentException: not a valid name: 
    at com.squareup.javapoet.MethodSpec.methodBuilder(MethodSpec.java:188)
    at com.fern.java.generators.UnionGenerator$ModelUnionSubTypes
        .getStaticFactory(UnionGenerator.java:440)
    at com.fern.java.generators.union.UnionTypeSpecGenerator
        .getStaticConstructors(UnionTypeSpecGenerator.java:162)

Root cause

This hits the same crash site as #17429 (UnionGenerator.java:440) but for a different reason. CasingConfiguration.computeName() builds the Java identifier by first calling toCamelCase(), which splits the input into words using this regex (splitWords):

[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+

That pattern doesn't match underscores at all, so splitWords("_") returns an empty list, and toCamelCase("_") returns "". The reserved-keyword check in sanitizeName() runs after this transformation, so there's no string left to compare against the keyword list by that point — computeName("_").camelSafe comes back as "", not "_" and not "__".

This is a distinct bug from #17429: that one was a missing entry in a keyword list; this one is the word-splitting step silently discarding a legal identifier before the keyword list is ever consulted. Just adding "_" to CasingConfiguration.JAVA_RESERVED_KEYWORDS on its own is a no-op — verified directly against the real class.

Worth noting: KeyWordUtils.java (a separate reserved-word list used for object property/class names elsewhere in the same generator) already treats _ as reserved and escapes it correctly there. It's specifically the CasingConfiguration word-splitting path (used for union discriminants, among other compressed/reconstructed names) that's affected.

Expected behavior

A valid, non-empty Java identifier — _ escaped to __, matching the existing convention used for other reserved words — while the serialized discriminator value stays "_".

Suggested fix

A complete fix needs both parts: (1) preserve all-underscore input during casing conversion instead of letting it collapse to "" (e.g. short-circuit before word-splitting when the input matches ^_+$), and (2) treat _ as a reserved Java identifier in JAVA_RESERVED_KEYWORDS so the preserved value gets escaped to __. I've implemented and verified this locally: unit tests against the real class confirm computeName("_").camelSafe == "__" (with "__"/"___" and ordinary names like "user_id" unaffected), and rebuilding the fernapi/fern-java-sdk image from source with the fix and re-running the reproduction above now generates successfully, with public static Filter __(UnderscoreFilter value) and @JsonTypeName("_") — a valid identifier, unchanged wire value. Happy to open a PR.

Note on parity: the canonical TypeScript CasingsGenerator has the same default behavior (_ also collapses to "" via lodash's camelCase() unless its preserveUnderscores option is set, which isn't currently invoked anywhere in the generator pipeline), and _ is likewise absent from reserved.ts's Java keyword set. So this isn't a case of Java being out of sync with existing TypeScript behavior — neither implementation handles it today. Bringing TypeScript to parity would be a separate, broader change.

Follow-up to #17429 / #17430, found while verifying whether that fix would also cover _ (it didn't, until now).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions