Skip to content

[scala-sttp4] Omit optional fields set to None instead of serializing null (circe)#24362

Open
vivekmahajan wants to merge 1 commit into
OpenAPITools:masterfrom
vivekmahajan:fix/scala-sttp4-drop-null-options
Open

[scala-sttp4] Omit optional fields set to None instead of serializing null (circe)#24362
vivekmahajan wants to merge 1 commit into
OpenAPITools:masterfrom
vivekmahajan:fix/scala-sttp4-drop-null-options

Conversation

@vivekmahajan

@vivekmahajan vivekmahajan commented Jul 20, 2026

Copy link
Copy Markdown

Fixes #24360.

The circe branch of the scala-sttp4 generator uses plain deriveEncoder, which serializes Option None as JSON null:

implicit val encoder: Encoder[Pet] = deriveEncoder
// Pet("rex", None) => {"name":"rex","tag":null}

The scala-sttp (sttp3) generator omits absent optional fields — its hand-rolled encoders build fields via t.tag.map(v => "tag" -> v.asJson) — as do the json4s branch (json4s omits None by default) and scala-sttp4-jsoniter (transientNone defaults to true). So clients migrating sttp3 → sttp4 with circe silently start sending explicit nulls, which strict servers (e.g. FastAPI/pydantic non-nullable optional fields) reject with 422.

Fix: append .mapJson(_.dropNullValues) to the generated circe encoders — plain models, inline oneOf members, and the discriminator-configured sealed-trait encoder — restoring sttp3 parity: None ⇒ field omitted. The wrapper-composition oneOf encoders delegate to member encoders and inherit the fix. Verified end-to-end: with this change Pet(name = "rex", tag = None).asJson.noSpaces == {"name":"rex"} and the regenerated scala-sttp4-circe petstore sample compiles.

Known limitation (unchanged from scala-sttp): Option cannot express an explicit JSON null for nullable: true properties; distinguishing null-vs-absent would require a tri-state type and is out of scope here.

Tests: Sttp4CodegenTest#verifyOptionalFieldsOmittedWhenNone with a new fixture (sttp4-optional-fields.yaml). Samples regenerated via ./bin/generate-samples.sh bin/configs/scala-sttp4*.yaml — only the circe sample changes (json4s/jsoniter are unaffected, confirmed by zero-diff regeneration).

PR checklist


Summary by cubic

Omit optional fields set to None in scala-sttp4 circe models by dropping null values during encoding. This restores sttp3 behavior and prevents sending JSON nulls that strict servers reject.

  • Bug Fixes
    • Appended .mapJson(_.dropNullValues) to all generated circe encoders (plain models, inline oneOf members, and discriminator-based sealed traits).
    • Added test verifyOptionalFieldsOmittedWhenNone with sttp4-optional-fields.yaml.
    • Regenerated scala-sttp4-circe petstore samples; other libraries (json4s, scala-sttp4-jsoniter) unchanged.

Written for commit 2a3769c. Summary will update on new commits.

Review in cubic

… null (circe)

The circe branch of scala-sttp4 uses plain deriveEncoder, which serializes
Option None as JSON null. The scala-sttp (sttp3) generator omits absent
optional fields (its hand-rolled encoders build fields via
t.field.map(...)), as do json4s and jsoniter-scala defaults - so clients
migrating sttp3 -> sttp4 silently start sending explicit nulls, which
strict servers reject for non-nullable optional properties.

Append .mapJson(_.dropNullValues) to the generated circe encoders (plain
models, inline oneOf members, and the discriminator-configured sealed
trait encoder), restoring sttp3 parity: None => field omitted.

Note: as in scala-sttp, Option cannot express an explicit JSON null for
'nullable: true' properties; representing null-vs-absent distinctly would
need a tri-state type and is out of scope.

Fixes OpenAPITools#24360

Made-With: pi

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 9 files

Re-trigger cubic

@wing328

wing328 commented Jul 22, 2026

Copy link
Copy Markdown
Member

thanks for the PR

cc @nikhilsu @bmironenko

@wing328

wing328 commented Jul 22, 2026

Copy link
Copy Markdown
Member

tested locally and the change looks good

[info] welcome to sbt 1.10.11 (Eclipse Adoptium Java 17.0.18)
[info] loading settings for project sc-build from plugins.sbt...
[info] loading project definition from C:\Users\User\AppData\Local\Temp\sc\project
[info] loading settings for project sc from build.sbt...
[info] set current project to openapi-client (in build file:/C:/Users/User/AppData/Local/Temp/sc/)
[info] Executing in batch mode. For better performance use sbt's shell
[info] compiling 1 Scala source to C:\Users\User\AppData\Local\Temp\sc\target\scala-2.13\classes ...
[success] Total time: 8 s, completed Jul 22, 2026, 3:07:09 PM

@wing328

wing328 commented Jul 22, 2026

Copy link
Copy Markdown
Member

if no one has any feedback, i'll merge it later this week

@nikhilsu

Copy link
Copy Markdown
Contributor

@wing328 @vivekmahajan do you think it would be better to make this behavior configurable?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][scala-sttp4] Optional model fields serialize as JSON null instead of being omitted (regression vs scala-sttp)

3 participants