GeezerVison is showing github.com — refactor: reduce boilerplate code · Issue #461 · spotify-web-api-java/spotify-web-api-java
refactor: reduce boilerplate code #461DescriptionMigration: Boilerplate Reduction
Summary
Incrementally modernize the project by:
- Migrating Maven → Gradle
- Introducing Kotlin
- Replacing boilerplate in models and requests
Strict requirement: No breaking changes to public API.
Execution Rules
- Work in small, reviewable commits
- Each step must:
- Compile successfully
- Pass all tests
- Do NOT combine phases in a single commit
- Prefer incremental migration over large rewrites
- Maintain Java ↔ Kotlin interoperability at all times
Phase 1: Gradle Migration
Tasks
- Create
build.gradle.kts
- Port dependencies from
pom.xml
- Add required plugins (
java, jacoco, publishing)
- Keep existing directory structure
- Update CI to use Gradle
Checkpoint
./gradlew build succeeds
- All tests pass
Commit Boundary
chore: migrate build system from Maven to Gradle
Phase 2: Enable Kotlin
Tasks
- Add Kotlin plugin and stdlib
- Verify mixed Java/Kotlin compilation works
Checkpoint
- Project builds with Kotlin enabled
- No functional changes
Commit Boundary
chore: add Kotlin support to project
Phase 3: Model Migration (Incremental)
Tasks
- Convert model classes to a Kotlin representation in small batches, respecting this library's public API stability as described by jakewharton.com/public-api-challenges-in-kotlin.
- Use kotlinx.serialization for JSON handling
- Remove custom JSON utilities only for migrated models
Execution Strategy
- Start with simple, dependency-free models
- Progress to dependent and complex models
Per-Batch Checkpoint
- Build succeeds
- Serialization/deserialization tests pass
- No API breakage
Commit Boundaries (repeat per batch)
refactor(models): convert <ModelName> to Kotlin data class
Phase 4: Request Layer Simplification
Tasks
- Introduce a mechanism to reduce request boilerplate:
- Option A: Code generation
- Option B: Shared abstractions/annotations
- Apply incrementally to request classes
Execution Strategy
- Start with simplest request classes
- Migrate complex ones later
Per-Batch Checkpoint
- Requests produce identical HTTP behavior
- Tests pass
Commit Boundaries (repeat per batch)
refactor(requests): simplify <RequestName> implementation
Phase 5: Remove Remaining JSON Boilerplate
Tasks
- Eliminate remaining custom JSON parsing
- Centralize common behavior if needed
Checkpoint
- All JSON fixtures deserialize correctly
- No regressions
Commit Boundary
refactor: remove remaining JSON utility boilerplate
Phase 6: Kotlin Conversion (Core)
Tasks
- Convert core classes (API entry point, base request classes, utilities) to Kotlin
- Improve readability using Kotlin features (null safety, defaults)
Checkpoint
- Public API unchanged
- Full test suite passes
Commit Boundaries
refactor(core): convert core classes to Kotlin
Phase 7: Tests & Quality
Tasks
- Migrate test utilities to Kotlin (optional but preferred)
- Improve readability and reduce duplication
- Ensure coverage is maintained or improved
Checkpoint
- All tests pass
- Coverage ≥ previous baseline
Commit Boundary
test: modernize and clean up test suite
Phase 8: Documentation & Release
Tasks
- Update README (Gradle + Kotlin usage)
- Document contribution workflow
- Prepare release notes
Checkpoint
- Docs reflect new setup accurately
Commit Boundary
docs: update documentation for Kotlin + Gradle migration
Final Validation
- Full build passes
- All tests pass
- No public API changes
- Artifacts publish successfully
Guidance
- Prefer safe refactors over clever optimizations
- If uncertain, keep existing behavior unchanged
- Avoid large diffs; split work into minimal steps
- Validate continuously after each change
ActivityWhosNickDoglio commented on May 16, 2026 Are you open to feedback or contributions for any of this? Recently started playing around with this library and have primarily worked in Kotlin/Gradle for a long time.
Convert model classes to Kotlin data classes in small batches
I'd suggest avoid data classes for things in the public API and instead use something like Poko, https://jakewharton.com/public-api-challenges-in-kotlin/
Replace manual JSON parsing with Jackson annotations
Do you have a strong preference for Jackson? kotlinx.serialization is first party and quite nice. Reacted by Jonas Thelemann dargmuesli commented on May 20, 2026 Super nice @WhosNickDoglio, very valuable insights! 💚 And of course kotlinx.serialization is preferred. I plan to kick off an agent run for this refactor (right after releasing the next major version including merging #460) and see how far it goes. Reacted by Nicholas Doglio dargmuesli commented on Nov 5, 2026 Status update
I re-evaluated the plan against the current codebase. Before #473 (10.0.0), most of the boilerplate was hand-written JSON parsing, and that PR already replaced it with reflective Gson. What remained was mostly Javadoc and Java-facing builders that have to stay for compatibility. Rewriting in Kotlin or moving to Gradle would not remove those, so I kept Java and Maven and went after the remaining boilerplate directly.
Done
| Plan item |
Outcome |
| "No breaking changes" as a hard requirement |
Now checked in CI: japicmp compares every build against the last stable release and fails on binary or source incompatible changes, except across a major version bump (#488, #491). |
| Phase 5: remove remaining JSON boilerplate |
Requests no longer go through per-model JsonUtil classes, and the new public ModelObjects (fromJson, arrayFromJson, pagingFromJson, pagingCursorbasedFromJson) covers callers who parse JSON themselves. In 10.x the JsonUtil classes are only deprecated (#489, no API change). For v11 they are removed, together with PlaylistItemFactory (#493). |
| Phase 4: request layer simplification, Option A (code generation) |
106 of 108 data request classes, and the SpotifyApi methods that create them, are generated from the vendored spec/openapi.yaml by codegen/generate.py, into their own source root src/generated/java (#487, #492). Each class is one line in codegen/requests.yaml, and CI fails when the generated code drifts from the spec. The generated API follows the spec where the hand-written one had drifted. Request bodies take List/Map instead of Gson types, and every change is listed in MIGRATION.md. |
| Phase 8: documentation |
CONTRIBUTING.md, AGENTS.md and MIGRATION.md are updated in the PRs above. Adding an endpoint is now one descriptor line plus a fixture, a test and an example. |
Decided against
| Plan item |
Why |
| Phase 1: migrate Maven → Gradle |
Brings nothing for a single-module library: Maven builds Kotlin fine if that's ever needed. It would mean rewriting the release path, CI, jitpack.yml, the separate examples project and the docs checks. |
| Phase 2, 3, 6: Kotlin models and core, kotlinx.serialization |
Conflicts with the no-breaking-changes requirement. Gson was part of the public API (requires transitive, Gson types in public signatures). Kotlin's getter naming changes existing names (getIs_playing() would become is_playing()). Every Java user would gain a kotlin-stdlib dependency. Data classes and Poko still need hand-written builders for Java callers, so the saving is small. The Javadoc pipeline (doclint, the published apidocs/) would also have to move to Dokka. |
| Phase 7: tests in Kotlin |
Follows from the above; the tests stay in Java and were updated alongside the changes. |
If Kotlin support is still wanted, I think the cleaner route is a separate, optional spotify-web-api-kotlin artifact (e.g. suspend wrappers over executeAsync(), Kotlin-friendly builders) on top of the Java library, so Java users get no new dependency. Thanks @WhosNickDoglio for the Poko and kotlinx.serialization pointers. They shaped this assessment, and they'd be the starting point for such a module.
Remaining
Generated by Claude Code
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|