how to build java sdk openapiEngineering

How to Build a Java SDK based on OpenAPI

Automate Java SDK creation using OpenAPI! Build, maintain, and deploy high-quality SDKs using best practices for API integration and developer experience.

Blog-Post-Author

Aleks

Created: September 7, 2024

Updated: September 27, 2024


Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.

1. Introduction#

APIs are one of the most important components in communication between services in modern software, and the OpenAPI Specification has become the go-to standard for defining them. But while building an API might seem straightforward, offering a smooth developer experience often depends on the availability of SDKs (Software Development Kits). SDKs simplify API integration by providing pre-built, language-specific libraries that developers can use to interact with APIs more efficiently.

Creating and maintaining SDKs manually for multiple programming languages can quickly become a time-consuming task. That's where automating the process as much as possible comes in handy. By leveraging the OpenAPI standard, you can automate the generation of SDKs, reducing maintenance time and ensuring consistency across different platforms.

In this guide, we’ll focus on how to build a Java SDK based on an OpenAPI specification. By the end of this tutorial, you’ll have a clear understanding of the following:

  • How to build a Java SDK based on an OpenAPI specification?
  • What are the necessary steps to go from an OpenAPI file to a full functional, deployable Java SDK?
  • What are the best practices for building and maintaining SDKs?

We'll use Corbado’s passkey-first Java SDK as an example, guiding you through the practical steps and considerations involved in creating a professional, user-friendly SDK.

2. Prerequisites#

The prerequisites are pretty simple. You need a backend services that exposes an API. This API should be a RESTful API and you should have an OpenAPI specification of it available.

3. Use an API Generator to Create a Client#

The first step in building a Java SDK from an OpenAPI specification is to use an OpenAPI generator to create a client SDK. The OpenAPI generator simplifies the process by automatically generating client-side code that interacts with your API.

Ideally, the generated client SDK should be placed in a separate project or repository. This ensures clean separation from your main application, helping to simplify dependency management. Generated SDKs often include third-party libraries that might not align with the dependencies used in your core project.

Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

Best Practice: Avoid Modifying Generated Code

It’s best practice to avoid manually modifying the generated code. Once you start editing the generated SDK code, it becomes difficult to keep up with future updates or regenerate the SDK when your OpenAPI specification changes. Instead, any custom functionality should be built on top of the generated code, not directly within it.

If you're concerned about exposing the complexities of the generated code to developers, decouple the generated code from your SDK interface. This way, the complexity remains hidden, and your SDK provides a clean, simple interface for developers to use.

4. Understand the SDK Requirements#

When building a Java SDK from an OpenAPI specification, it’s important to first familiarize yourself with existing SDKs that can serve as role models (if they exist - in other cases you need to define the requirements for the SDK).

4.1 Key Use Cases: Session Validation and User Information Extraction#

For Corbado’s Java Passkeys SDKs, two key scenarios include session validation and extracting additional user information. These features are important for developers integrating user authentication into their applications.

4.2 Purpose of the SDK: Why Not Just Use a Generated Client?#

The goal of the SDK is to provide more than just a generated client. While a simple client offers basic API interaction, a well-built SDK enhances the developer experience by offering:

  • A simple, easy-to-install library for connecting to the backend.
  • Meaningful error handling, making it easier for developers to debug and resolve issues.
  • Easy configuration, allowing developers to get started with minimal friction.
  • Clear, documented examples for all available functionality (often provided through unit tests).
  • A lower entry barrier for developers trying out the service for the first time.
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

4.3 JWT/JWK Handling and Validation#

An essential part of building the Corbado SDK is understanding how JSON Web Tokens (JWT) and JSON Web Keys (JWK) are handled, as these are used for session validation. You’ll want to:

  • Familiarize yourself with JWT/JWK concepts
  • Search for available JWT libraries for Java

A JWT validation process could include validating the signature, checking token expiration, and handling JWK errors with custom exceptions like JWTVerificationException or JwkException.

private SessionValidationResult getAndValidateUserFromShortSessionValue(final String shortSession) throws JWTVerificationException, JwkException, IncorrectClaimException { if (shortSession == null || shortSession.isEmpty()) { throw new IllegalArgumentException("Session value cannot be null or empty"); } try { // Decode the JWT without verifying it, to extract its claims and headers. DecodedJWT decodedJwt = JWT.decode(shortSession); // Retrieve the signing key (JWK) using the Key ID (kid) from the JWT header. final Jwk jwk = this.jwkProvider.get(decodedJwt.getKeyId()); // If the signing key (JWK) is not found, throw a custom exception. if (jwk == null) { throw new SigningKeyNotFoundException(shortSession, null); } // Extract the RSA public key from the JWK for verifying the JWT. final RSAPublicKey publicKey = (RSAPublicKey) jwk.getPublicKey(); // Define the RSA256 algorithm using the retrieved public key. final Algorithm algorithm = Algorithm.RSA256(publicKey); // Create a JWT verifier using the algorithm and expected issuer. final JWTVerifier verifier = JWT.require(algorithm).withIssuer(this.issuer).build(); // Verify the JWT signature and decode the JWT. decodedJwt = verifier.verify(shortSession); // Build and return the session validation result, extracting claims like name and userID. return SessionValidationResult.builder() .fullName(decodedJwt.getClaim("name").asString()) // Extract the 'name' claim. .userID(decodedJwt.getClaim("sub").asString()) // Extract the 'sub' (user ID) claim. .build(); } catch (final IncorrectClaimException e) { // Handle cases where the JWT's claims do not match expected values (e.g., incorrect issuer). // If the claim that caused the exception is the 'iss' (issuer) claim. if (StringUtils.equals(e.getClaimName(), "iss")) { final String message = e.getMessage() + " Be careful of the case where issuer does not match. " + "You have probably forgotten to set the cname in config class."; // Rethrow the IncorrectClaimException with the updated message. throw new IncorrectClaimException(message, e.getClaimName(), e.getClaimValue()); } // If the exception is not related to the issuer, rethrow the original exception. throw e; } catch (final JwkException | JWTVerificationException e) { // Catch any errors related to obtaining the JWK or verifying the JWT. // Rethrow these exceptions as they are critical and need to be handled by the caller. throw e; } }

Example endpoint in Spring Boot using Session Service:

@RequestMapping("/profile") public String profile( final Model model, @CookieValue("cbo_short_session") final String cboShortSession) { try { // Validate user from token final SessionValidationResult validationResp = sdk.getSessions().getAndValidateCurrentUser(cboShortSession); // get list of emails from identifier service List<Identifier> emails; emails = sdk.getIdentifiers().listAllEmailsByUserId(validationResp.getUserID()); model.addAttribute("PROJECT_ID", projectID); model.addAttribute("USER_ID", validationResp.getUserID()); model.addAttribute("USER_NAME", validationResp.getFullName()); // select email of your liking or list all emails model.addAttribute("USER_EMAIL", emails.get(0).getValue()); } catch (final Exception e) { //Handle verification errors here model.addAttribute("ERROR", e.getMessage()); return "error"; } return "profile"; }

Providing clear unit tests for valid and invalid tokens can demonstrate how these validations work in practice. This not only ensures robustness but also offers developers a clear example of how to handle JWTs in their applications. You can take a look at SessionServiceTest as an example.

4.4 Handling Errors in the SDK#

Error handling is crucial to any SDK. Some of the key exceptions in our example include:

  • CorbadoServerException: Thrown when the backend API returns an HTTP status code other than 200, this exception includes deserialized error responses, making additional error information easily accessible.
  • StandardException: Used for client-side errors, unrelated to the backend.
  • JWTVerificationException and JwkException: Specific to session validation errors related to JWT or JWK.

4.5 Implementing Client-Side Validation#

The SDK also takes care of validating input on the client side to avoid sending invalid data to the server. Common validations include:

  • URL validation: Ensuring that provided URLs follow the correct format.
  • String constraints: For instance, ensuring that user IDs are not empty.
  • Configuration validation: Certain fields, like projectId and apiSecret, should follow specific patterns (e.g., projectId starting with pro- and apiSecret starting with corbado1_).

5. Identify Best Practices for Java#

Follow Java best practices for project structure and code quality is essential to ensure that your library is easy to use, maintain, and integrate. Below are some recommendations for structuring your SDK project and maintaining clean, readable code, with specific examples for Java.

5.1 Structuring Your Project for Ease of Use#

A well-structured project is crucial for making your Java SDK standalone and easily importable by other developers. Look at this repository on GitHub, which provides sample project layouts that follow best practices. Organizing your SDK into modular components with clear directory structures helps developers navigate the code more easily and import your SDK without hassle.

how to structure java projects

5.2 Use Modern Formatters and Linters#

To maintain clean, standardized code, it’s important to use modern formatters and linters. These tools ensure that your code adheres to established guidelines, making it more readable, consistent, and less prone to errors. Below is a recommendation for Java:

5.3 Keep Code Clean and Maintainable#

Clean code is critical for any SDK, but it becomes even more important in smaller projects where every line of code has the potential to impact usability and maintainability.

6. Identify Best Practices and Established Coding Standards for SDK Development#

To build a nice Java SDK, look at how established companies approach SDK development. For example, Stripe is known for having one of the most well-documented and widely used SDKs across multiple languages. Studying their approach can teach you the following:

  • CI/CD Structure: Stripe uses an efficient continuous integration and continuous delivery (CI/CD) pipeline to automate tests and deployment. Understanding their setup can help you build a basic CI/CD structure that ensures your SDK is always tested and ready for production.
  • What to Test in CI/CD: By observing what major SDKs like Stripe include in their test suite, you’ll learn what is critical for ensuring compatibility, such as unit tests, integration tests, and API contract validation. Testing should cover core functionality, edge cases, and backward compatibility.
  • Language Version Compatibility: Ensuring compatibility with multiple versions of your target language is essential for a widely used SDK. For Java, you might support both Java 8 and above, depending on user requirements.
  • Linting and Code Static Analysis: Stripe implements strict linting and code static analysis to maintain high code quality. Tools like Checkstyle for Java can be integrated into your CI pipeline to catch potential issues before they reach production.

6.1 Code and Project Structure#

Examining how SDKs like Stripe structure their projects can give you insights into creating a clean, modular project architecture. For example, in Java, Stripe uses the Builder Pattern for their client configuration classes, which is particularly useful when dealing with many optional parameters. This pattern ensures that your SDK’s API remains flexible while maintaining readability and simplicity for developers. If your SDK has configuration-heavy components, adopting this pattern might be ideal.

6.2 Documentation Best Practices#

One of the key takeaways from analyzing successful SDKs is the importance of comprehensive and accessible documentation. The README file is often the first interaction a developer has with your SDK, so it should include:

  • Clear setup instructions: How to install and configure the SDK with minimal friction.
  • Code examples: Demonstrating core use cases, such as authentication, session handling, and error management.
  • Links to further documentation: For developers who need more detailed instructions or wish to explore advanced use cases.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

7. Determine Your Tech Stack and Useful Libraries#

Building an SDK requires selecting the right libraries to ensure compatibility, maintainability, and ease of use. The stack you choose will directly affect how well your SDK integrates into other projects and how easy it is for developers to adopt it. Let’s break down the key components you’ll need to consider based on the insights gathered from previous sections and established best practices.

7.1 Build Tools and Dependency Management#

For Java, the two main build tools to consider are Maven and Gradle. Both are widely used, but your choice depends on the specific needs of your SDK:

  • Maven: If you're more familiar with Maven and don't need custom build logic, it’s a solid, well-established option. Maven is known for its simplicity, strong dependency management, and extensive ecosystem. It integrates easily with package distribution platforms like Maven Central.

maven

  • Gradle: Gradle offers more flexibility and faster build times, especially for larger projects. It can be useful if your SDK requires more advanced build customizations or if you prioritize build performance.

gradle

7.2 HTTP Client and JSON Parsing#

Your SDK will need a reliable HTTP client to interact with APIs. For Java, consider HttpClient from the standard library or popular libraries like OkHttp if you need more flexibility.

okhttp

For JSON parsing, both Jackson and Gson are widely used in Java. Jackson is often favored for its extensive features and performance, while Gson is lighter and simpler to use. If your SDK needs advanced serialization features or works with complex JSON structures, Jackson is likely the better choice.

7.3 Logging and Error Handling#

For logging, Java developers often choose between SLF4J with Logback or Log4j. SLF4J provides an abstraction layer that allows you to switch between logging implementations without changing your code. Coupled with Logback, it offers high performance and flexibility. Log4j is another solid option but has become less popular due to past security vulnerabilities.

log4j

When handling errors, it's important to consider how your SDK will interact with external systems. For example, using a custom exception like CorbadoServerException can provide meaningful error messages when something goes wrong during API calls. This is especially useful when developers need to debug integrations with your SDK.

7.4 Testing Frameworks#

Testing is critical to maintaining the reliability of your SDK. For Java, tools like JUnit and Mockito are standard for unit testing and mocking. JUnit provides a simple, structured way to write tests, while Mockito allows you to mock objects in tests, which is particularly useful for API-driven SDKs where you need to simulate API responses.

7.5 CI/CD Integration#

Continuous integration and delivery (CI/CD) are essential for ensuring that your SDK is always deployable and free of bugs. Popular options include:

  • GitHub Actions: Integrated into GitHub, making it a convenient option if your SDK is hosted there. It’s flexible, highly customizable, and free for open-source projects.
  • Jenkins: A self-hosted solution that provides more control but requires additional setup and maintenance.
  • Travis CI: A cloud-based CI/CD service that’s particularly popular with open-source projects.

Whichever you choose, make sure your pipeline includes automated testing, linting, and code analysis.

For Corbado's Java SDK, we used the following CI/CD process:

The SDK should be compatible with most used versions of the language. CI/CD can easily do that with matrix feature that allows you to run multiple jobs in parallel with different configurations.

This often helps identify compatibility or configuration issues that may not be apparent locally if the build or tests are failing.

Simplify the deployment process for other developers by following these steps: After build, lint and test you can choose to deploy the application on some events (for example on version tag in Java SDK). No extra steps needed. For Java, we needed to:

  1. Register at Central Portal.
  2. Create a GPG Key to sign the build.
  3. Create a deployment job in the GitHub Actions workflow.
  4. Make sure the projects comply with Central requirements.
  5. Follow Centrals publishing guide for Maven
  6. Configure GPG
  7. Make a temporary Maven set up (needed by central publishing plugin)
  8. Sign and publish the artifact

7.6 Optional Tools#

Libraries like Lombok can improve development efficiency and reduce boilerplate code. It can greatly reduce boilerplate code by automatically generating getters, setters, constructors, and builders at compile time using annotations.

Lombok

7.7 Dependency Management#

Managing dependencies is a key aspect of maintaining a clean, lightweight SDK. Avoid adding unnecessary dependencies, as they can bloat your package and potentially introduce security vulnerabilities. Be mindful of separating compile-time, runtime, and test dependencies. For example, in Maven, use the <scope> tag to define when a dependency is required.

7.8 Security and Compatibility Concerns#

When selecting libraries, also take into account security vulnerabilities and version compatibility. For example, older versions of dependencies may no longer receive security updates, which can introduce risks to your users. Be cautious with which versions of dependencies you support and ensure that your SDK runs on the most commonly used versions of the language.

StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

8. Ease of Collaboration and Development#

When building an SDK, ensuring a smooth and collaborative development process is key to long-term success. This requires setting up consistent configurations, automating processes where possible, and simplifying release management to make it easy for teams to work together and ship updates.

8.1 Consistent Project Configuration Across All Machines#

A full CI/CD pipeline is essential for ensuring that all code is automatically tested and analyzed before it reaches production. However, to make collaboration as seamless as possible, every developer working on the SDK should use the same coding standards and tools, regardless of their machine or development environment.

By sharing these configuration files through version control, developers won’t need to reconfigure their IDEs or tools every time they clone the project. This ensures consistency in code quality, reduces friction in onboarding new developers, and prevents errors caused by mismatched environments.

8.2 Sharing IDE Configuration#

If possible, provide IDE-specific configuration files for your language and preferred IDEs, such as Eclipse, IntelliJ IDEA, or VS Code. This will help ensure that developers are using the same environment settings, making collaboration more efficient. For Java projects, you could include an .idea folder for IntelliJ IDEA or .project for Eclipse in your repository to share project settings.

Providing pre-configured settings files simplifies the development process by ensuring that all contributors work in an optimized and uniform environment.

8.3 Version Management and Simplified Releases#

Managing the SDK version should be straightforward and clear. If your versioning process is complicated or error-prone, it can lead to mistakes during release preparation, resulting in inconsistencies across different environments.

A best practice is to make the VERSION file the single source of truth for your SDK version. This simplifies the release process by ensuring that all tooling, documentation, and package metadata pull from the same version reference. By keeping the version in one place, you reduce the risk of mismatches between what’s deployed and what’s documented.

For example, you can update the VERSION file automatically during your CI/CD pipeline to reflect the latest release version. This practice can also trigger other actions, such as updating changelogs and pushing the latest version to package managers like Maven Central.

8.4 Automating Development Workflows#

Automation plays a key role in maintaining ease of collaboration. Set up pre-commit hooks that run linters, formatters, and tests before changes are committed to the codebase. This will ensure that only clean, validated code is pushed, reducing the chance of introducing errors into the shared repository.

For example:

  • Husky can be used to set up Git hooks that run scripts before code is committed.

Husky

  • Prettier (for JavaScript) can automatically format code as part of the commit process, ensuring consistent styling across all commits.

Prettier

9. Testing#

Testing is a critical part of building any SDK, ensuring that it works as expected and is reliable in production environments.

When building your SDK, it’s efficient to start by referencing existing tests from similar SDKs. For example, if Corbado already has SDKs written in other languages like PHP, use those as a baseline. You can adapt and extend the existing test cases to match the language-specific features of your new SDK.

  • Start by examining the key use cases covered by the existing tests, such as session validation, error handling, and JWT verification.
  • Replicate those tests in your new SDK, making adjustments for language differences.
  • If your SDK introduces new functionality, be sure to add additional tests that cover these features.

To ensure that your SDK delivers a great developer experience, it's essential to approach testing from the perspective of an external developer who will be using it. Set up the SDK as if you were an external user, integrating it into a sample application to identify any pain points or unclear areas.

10. Documentation#

Clear, concise, and well-structured documentation is crucial to the success of any SDK. It ensures that developers can easily understand how to implement your SDK, use its features, and troubleshoot issues. The documentation should not only provide a comprehensive overview of the SDK's capabilities but also guide developers through its use with real-world examples.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

10.1 Ensure Users Understand Function Arguments and Configuration Fields#

Your documentation must clearly outline what is required for each function and configuration field. Developers need to know:

  • Which arguments are required: Make it clear in the documentation which function parameters or configuration fields are mandatory and which are optional. Specify the default values for any optional parameters, if applicable.
  • What constraints apply to each field: For example, is the field nullable? Are there maximum or minimum length requirements? Is the field expected to follow a certain format (e.g., email, URL)? These details should be explicitly stated to avoid confusion and potential errors during implementation.

To ensure consistency, the information in your documentation should be automatically generated from your code annotations, such as Javadoc for Java. This ensures that the documentation is always up-to-date and accurately reflects the SDK’s implementation.

10.2 Clearly Explain Functions, Classes, and Configuration Parameters#

Your documentation should provide detailed explanations of what each function and class does. This includes:

  • The purpose of the function or class.
  • What parameters it expects and their types.
  • The expected output or return type.
  • Any side effects the function may have (e.g., database changes, API calls).
  • Specific error conditions or exceptions the user should handle.

By providing clear and descriptive documentation, you lower the barrier to entry for developers who are new to your SDK, making it easier for them to understand how to integrate it into their projects.

10.3 Follow Standard Code Commenting Practices#

Beyond formal documentation, clear code comments are essential to help future developers (including your team) understand the logic behind certain code decisions. Use standard commenting practices in your codebase to:

  • Explain complex logic or edge cases that might not be immediately obvious.
  • Mark TODOs or areas that need further attention.
  • Provide additional context when handling errors or exceptions.

Good commenting practices help bridge the gap between the code and its documentation, making the project easier to maintain and extend.

11. Conclusion#

Building a Java SDK from an OpenAPI specification involves several key steps, from generating the client to ensuring ease of use for developers through proper project structure, best practices, and thorough testing. By focusing on a clean, consistent technology stack, adopting industry-standard tools and libraries, and providing robust documentation, you create an SDK that’s intuitive, reliable, and easy to integrate. Following these best practices not only streamlines development but also positions your SDK as a professional tool that developers can confidently use in production.

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free