Building a seamless connection between a TypeScript application and a Java application can feel like bridging two worlds. Both languages have strong typing capabilities, but ensuring that they adhere to the same contract can be challenging. In my experience, the best approach is to define a shared schema that governs the data structures and enforce it in both ecosystems. Here, I’ll walk you through how I tackled this problem, leveraging JSON Schema as the common ground and tools like json-schema-to-typescript
and jsonschema2pojo
to maintain consistency.
Thank me by sharing on Twitter 🙏
The Problem: Maintaining a Consistent Contract
Imagine you have two applications that need to communicate reliably. Your TypeScript frontend sends data to a Java backend, or vice versa. Without a shared contract, mismatches in the data structure could lead to runtime errors that are frustrating to debug. For example, a missing required field or a type mismatch could cause an application to crash or behave unexpectedly. That’s why having a formal, shared schema is crucial.
By generating types and classes directly from a schema, you eliminate the guesswork and make sure both applications remain in sync.
Step 1: Define the Shared Schema
The first step is creating a JSON Schema that represents the data structure you want to share. JSON Schema is language-agnostic and widely supported, making it perfect for this task. For instance, let’s say I want to define a User
object. I created a file named User.schema.json
with the following content:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User",
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["id", "name", "email"]
}
This schema specifies that a User
object must have an id
, a name
, and an email
. The email
field even includes a format validator for added assurance.
TECKNET Wireless Mouse, 2.4G Ergonomic Optical Mouse, Computer Mouse for Laptop, PC, Computer, Chromebook, Notebook, 6 Buttons, 24 Months Battery Life, 2600 DPI, 5 Adjustment Levels
$8.49 (as of January 9, 2025 10:16 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Chip War: The Quest to Dominate the World's Most Critical Technology
$17.71 (as of January 11, 2025 10:31 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Highwings 8K 10K 4K HDMI Cable 48Gbps 6.6FT/2M, Certified Ultra High Speed HDMI Cable Braided Cord-4K@120Hz 8K@60Hz, DTS:X, HDCP 2.2 & 2.3, HDR 10 Compatible with Roku TV/PS5/HDTV/Blu-ray
$9.99 (as of January 9, 2025 10:16 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Step 2: Generate TypeScript Types
To use the schema in my TypeScript application, I turned to json-schema-to-typescript
. This tool converts a JSON Schema into a TypeScript definition file, ensuring type safety on the frontend. Here’s how I did it:
- Install the tool:
npm install -g json-schema-to-typescript
- Run the command to generate a TypeScript file:
json-schema-to-typescript --input User.schema.json --output User.ts
The result was a User.ts
file containing the following code:
/**
* User
*/
export interface User {
id: string;
name: string;
email: string;
}
Now, I can confidently use the User
type in my TypeScript code. Any deviation from the schema will be caught during development.
Step 3: Generate Java Classes
On the Java side, I used jsonschema2pojo
, a powerful tool for generating Java classes from JSON Schema. This ensured the backend adhered to the same structure as the frontend. Setting it up was straightforward:
- Install
jsonschema2pojo
(I used Homebrew):brew install jsonschema2pojo
- Run the command to generate a Java class:
jsonschema2pojo --source User.schema.json --target java/src --package com.example.model
This generated a User.java
file with the following code:
package com.example.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("email")
private String email;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
The generated Java class mirrors the JSON Schema, ensuring the backend aligns perfectly with the frontend.
Step 4: Validate Data at Runtime
While generating types and classes is a great start, I wanted to add an extra layer of safety by validating data at runtime. For this, I used the following tools:
In TypeScript: I used Zod, a TypeScript-first schema validation library.
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
});
type User = z.infer<typeof UserSchema>;
const user: User = { id: "123", name: "Alice", email: "alice@example.com" };
try {
UserSchema.parse(user);
} catch (e) {
console.error("Validation failed", e);
}
In Java: I used Jackson’s support for JSON Schema validation.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setId("123");
user.setName("Alice");
user.setEmail("alice@example.com");
JsonSchema schema = mapper.schemaFor(User.class);
// Validate user against schema (implementation may vary)
These validators ensure that even if something goes wrong during communication, the schema enforces correctness at runtime.
Step 5: Automate the Workflow
To keep everything synchronized, I automated the type and class generation in the CI/CD pipeline. Whenever the schema changes, the pipeline regenerates the TypeScript and Java files, ensuring that both applications stay up-to-date.
For example, in my CI/CD configuration, I included steps like:
- Running
json-schema-to-typescript
to generate TypeScript types. - Running
jsonschema2pojo
to generate Java classes. - Running tests to validate data against the schema in both environments.
Conclusion
By defining a shared JSON Schema and using tools like json-schema-to-typescript
and jsonschema2pojo
, I created a reliable contract between my TypeScript and Java applications. This approach eliminated guesswork, caught errors early, and ensured that both systems communicated seamlessly. While the initial setup took some effort, the long-term benefits of consistency and type safety made it well worth it.