How are SerializersModules composed, scoped to a format, and combined when multiple libraries each contribute one?
answer
- module1 + module2 / include for union
- overwriteWith for precedence on conflicts
- Duplicate (Base,Subtype) => IllegalArgumentException
- Attached per format instance (Json/Cbor)
- Libraries export modules; app merges them
basics
~10 sYou can merge several modules into one with the plus operator, attach the result to a format like Json, and use overwriteWith when two modules register the same type and one should win.
solid answer
~40 sModules are values you compose. Combine them with module1 + module2 (the plus operator) or SerializersModule { include(other) }; conflicting registrations of the same base/subtype pair throw an IllegalArgumentException unless you use a.overwriteWith(b) to let b take precedence. The merged module is attached per format instance via Json { serializersModule = ... }, so different formats can have different polymorphic scopes. Polymorphic scopes are keyed by the base class, so the same subtype can belong to several base hierarchies. Library authors typically expose a public SerializersModule that consumers include in their app-level module. The default is EmptySerializersModule(); the runtime resolves serializers by walking the configured module, falling back to plugin-generated serializers for concrete types.
code
kotlin · 9 linesval libModule = SerializersModule {
polymorphic(Animal::class) { subclass(Dog::class) }
}
val appOverrides = SerializersModule {
polymorphic(Animal::class) { subclass(Dog::class, CustomDogSerializer) }
}
// app wins on the Dog conflict instead of throwing:
val module = libModule.overwriteWith(appOverrides)
val json = Json { serializersModule = module }go deeper
Knows you attach a module to Json and that modules can be combined.
Uses plus/include and understands per-format scoping.
Knows conflict semantics, overwriteWith precedence, and multi-base subtype registration.
Designs a layered module strategy where libraries export modules and the app composes/overrides them cleanly across formats.
## Modules are first-class values A `SerializersModule` is immutable once built. You build with `SerializersModule { }`, and compose existing ones. ## Combining ```kotlin val combined = networkModule + domainModule // plus operator val included = SerializersModule { include(domainModule) } ``` - **`plus` / `include`**: union of registrations. If both register the *same* `(Base, Subtype)` pair, you get `IllegalArgumentException: ... is already registered` — conflicts are loud by design. - **`overwriteWith`**: `base.overwriteWith(override)` lets the right-hand module silently win on conflicts — use when an app must override a library's default serializer. ## Scoping to a format A module only takes effect through a format instance: ```kotlin val json = Json { serializersModule = combined } val cbor = Cbor { serializersModule = combined } ``` Each format instance carries its own module, so you can have different polymorphic tables for JSON vs. CBOR, or a strict module for external APIs and a lenient one internally. ## Library composition pattern ```kotlin // library exposes: val PaymentsSerializersModule = SerializersModule { polymorphic(PaymentMethod::class) { subclass(Card::class) } } // app composes: val appModule = PaymentsSerializersModule + ShippingSerializersModule ``` This is the idiomatic way multiple libraries each ship a module and the application merges them. ## Scope keying Polymorphic registrations are keyed by base type, so one concrete class can be a subtype under several bases (e.g. both `Animal` and `Pet`). The runtime resolves by `(staticBaseType, discriminator)`. ## Resolution order When serializing, the format consults its `serializersModule` for contextual/polymorphic lookups; for plain concrete `@Serializable` types it uses the compiler-generated serializer directly. `EmptySerializersModule()` is the no-op default. ## Gotchas - Forgetting to attach the combined module to *every* format instance you use. - Silent surprises from `overwriteWith` masking a real conflict. - Registering the same subtype twice under one base (duplicate) throws.
- What happens if two modules register the same subtype under the same base via plus?It throws IllegalArgumentException for a duplicate registration. Use overwriteWith to let one side win deliberately.
- Can the same concrete class be registered under two different base types?Yes. Polymorphic scopes are keyed by base type, so a class can appear as a subtype of multiple hierarchies in the same module.
saying these in an interview costs you the question
- Thinking plus silently resolves conflicts (it throws)
- Assuming one global mutable module instead of per-format immutable values
- Not knowing overwriteWith exists for intentional overrides
- Believing a module applies globally without attaching to a format
- Confusing scope keying — thinking a subtype can only belong to one base