Skip to content

Fix enum serialization (names instead of toString), value enums in kebs-baklava, add Seq columns to kebs-slick - #593

Merged
luksow merged 3 commits into
masterfrom
fix/enum-names-and-seq-columns
Sep 26, 2026
Merged

luksow merged 3 commits into
masterfrom
fix/enum-names-and-seq-columns

Conversation

@luksow

@luksow luksow commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Enum support serialized entries with toString. For Enumeratum enums that's not the entry's name whenever entryName is overridden or a casing mixin like EnumEntry.Uppercase / EnumEntry.Snakecase is used. So an entry declared as case object Active extends Status { override val entryName = "is-active" } was written as "Active" to JSON and to the database, and couldn't be read back. No existing test used a custom entryName, so this went unnoticed. It came up while migrating a large codebase from kebs 1.9.7, where JSON and DB values would have silently changed.

While fixing baklava's enum params, I also found that its value enum support is inverted, so this PR fixes that too and adds the module's first tests.

Enum names

  • Writers use getName. Covers spray-json, play-json, circe (the upper/lowercase variants; its default encoder already did this), slick (enum, List and hstore mappings, all casing variants), doobie, http4s path/query params, and baklava params/schemas (resolving the todo use getName comments). Enum deserialization error messages now list names too, so they show is-active rather than Active.
  • EnumLike ignore-case lookups use names: withNameIgnoreCase, withNameIgnoreCaseOption, valueOfIgnoreCase.
  • New EnumLike.apply(entries, name) and ValueEnumLike.apply(entries), used by all derivations (Enumeratum on Scala 2 and 3, scala.Enumeration, Scala 3 enums):
    • Lookup maps are computed once. Previously the derived instances rebuilt valuesToNamesMap and the reverse map on every call, e.g. on every JSON read or write.
    • values keeps declaration order. Previously it came from a Map's keys, so fromOrdinal / indexOf were wrong for enums with more than 4 entries.
  • Enumeratum derivation (Scala 2) aborts for types without a companion, e.g. a case object's own type, instead of expanding to code that doesn't compile.

Value enums in kebs-baklava

The value enum schema and params were built the wrong way round. valueEnumLikeSchema[T, V <: ValueEnumLikeEntry[T]] produced a Schema[T] for the value type (e.g. Int) and needed a Schema[V] of the enum. The params did the same (ToQueryParam[Int] built from ToQueryParam[Level]). The schema also listed entries' toString instead of their values. For a value enum Level(value: Int) with Low(1) and High(10), which kebs JSON formats send as 1 / 10:

  • Wrong docs. The kebs schema was never picked, so baklava's generic derivation documented Level as a string enum "Low" | "High".
  • No params. Value enums couldn't be used as query, path or header params: no instance existed, so it didn't compile.

The instances are now keyed on the enum type:

  • Schema[E] built from Schema[V] (e.g. integer / int32), with enum set to the entries' values;
  • ToQueryParam[E] / ToPathParam[E] / ToHeader[E] built from the value type's instances; header parsing goes back through withValueOption.

The erased signatures are unchanged, so MiMa is clean. The old instances couldn't be used, so no working code depends on them.

The value class and instance converter schemas now also keep the underlying additionalPropertiesSchema, as baklava's own wrapper schemas do. Before, a value class wrapping a Map lost its value schema.

Seq column types in kebs-slick

Stored like List columns (kebs 1.x supported them):

  • for enums, in KebsEnumImplicits and the casing variants;
  • for value classes and instance types, in a new opt-in trait KebsSeqImplicits. It's opt-in because on Scala 3 these instances break type inference of slick-pg array extension methods (@>) on List columns; SlickPgArrayTests / SlickPgHstoreTests fail on Scala 3 with them in the default traits.

Tests

  • New tests with a custom entryName and an EnumEntry.Uppercase enum:
    • EnumeratumEntryNameTest: names, lookups, declaration order, and no instance for a case object's type;
    • spray-json, circe and play-json formats, including the upper/lowercase variants;
    • SlickEnumEntryNameTests: column, List/Seq and hstore mappings.
  • Seq columns in SlickPgArrayColumnTypeTests.
  • kebs-baklava had no tests. It now has tests for all its instances:
    • params and schemas for enums (all casing variants) and value enums;
    • params for value classes and instance converters;
    • schemas for value classes and instance converters: the date/uuid/uri formats, default, description and additionalPropertiesSchema.

Against the code without the fix, the new Enumeratum tests fail (3 of 4).

Verified locally

  • tests of core, enumeratum, enum, spray-json, play-json, circe, slick, doobie, http4s, http4s-stir, pekko-http and baklava pass on 2.13.18 and 3.3.8 (akka-http on 2.13); kebs-baklava was also checked with CI=true (fatal warnings);
  • mimaReportBinaryIssues is clean against 2.1.6 and 2.2.1;
  • scalafmt checks pass.

Behaviour changes

  • Enumeratum enums with a custom entryName now serialize with that name (the documented behaviour) instead of toString. Scala 3 enums and scala.Enumeration are unaffected, since their name is toString.
  • kebs-baklava users with value enums get a correct schema (the value type with the entries' values) instead of baklava's string-enum derivation.

Enum formats, column types and params serialized entries with `toString`, which
differs from the entry name for Enumeratum enums with a custom `entryName` or a
casing mixin (e.g. `EnumEntry.Uppercase`).

- use `EnumLike.getName` in spray-json, play-json, circe, slick, doobie, http4s
  and baklava, and names in enum error messages
- look up case-insensitive names by name in `EnumLike`
- add `EnumLike.apply` / `ValueEnumLike.apply`, used by all derivations: lookup
  maps are computed once and `values` keeps declaration order
- abort Enumeratum derivation for types without a companion (e.g. a case object)
- add `Seq` column types: for enums by default, for value classes and instance
  types in the opt-in `KebsSeqImplicits`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mQaz4h7dYNSiY6ZFvg1fs
@mergify

mergify Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

luksow and others added 2 commits September 26, 2026 14:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mQaz4h7dYNSiY6ZFvg1fs
Value enum instances were built on the value type from instances of the enum
type (e.g. `Schema[Int]` from `Schema[Level]`), so value enums were documented
by baklava's generic derivation as string enums of entry names and couldn't be
used as query, path or header params. They are now built on the enum type from
instances of the value type, listing entry values.

Value class and instance converter schemas also keep the underlying
`additionalPropertiesSchema`, as baklava's own wrapper schemas do.

Add the first tests for kebs-baklava, covering all its instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017mQaz4h7dYNSiY6ZFvg1fs
@luksow luksow changed the title Use enum names instead of toString, add Seq columns to kebs-slick Fix enum serialization (names instead of toString), value enums in kebs-baklava, add Seq columns to kebs-slick Sep 26, 2026
@luksow
luksow merged commit 7d9a288 into master Sep 26, 2026
17 checks passed
@luksow
luksow deleted the fix/enum-names-and-seq-columns branch September 26, 2026 14:55
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