Building a seamless connection between a TypeScript application and a C# 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 NJsonSchema
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 C# 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.
Start with Why: How Great Leaders Inspire Everyone to Take Action
$10.49 (as of February 25, 2025 13:13 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.)Python Crash Course, 3rd Edition: A Hands-On, Project-Based Introduction to Programming
$28.99 (as of February 25, 2025 13:13 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.)Elon Musk
$22.96 (as of February 25, 2025 13:13 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 C# Classes
On the C# side, I used NJsonSchema
, a library that supports generating C# classes from JSON Schema. This ensured the backend adhered to the same structure as the frontend. Setting it up was straightforward:
1. Install NJsonSchema
(via NuGet):
dotnet add package NJsonSchema.CodeGeneration.CSharp
2. Use the library to generate a C# class:
using NJsonSchema;
using NJsonSchema.CodeGeneration.CSharp;
var schema = await JsonSchema.FromFileAsync("User.schema.json");
var generator = new CSharpGenerator(schema);
var fileContent = generator.GenerateFile();
System.IO.File.WriteAllText("User.cs", fileContent);
This generated a User.cs
file with the following code:
using System;
using Newtonsoft.Json;
public class User
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
}
The generated C# 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 C#: I used Newtonsoft.Json.Schema
for JSON Schema validation.
using Newtonsoft.Json;
using Newtonsoft.Json.Schema;
var schema = JSchema.Parse(System.IO.File.ReadAllText("User.schema.json"));
var json = JsonConvert.SerializeObject(new User { Id = "123", Name = "Alice", Email = "alice@example.com" });
var isValid = JObject.Parse(json).IsValid(schema);
if (!isValid)
{
Console.WriteLine("Validation failed");
}
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 C# 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
NJsonSchema
to generate C# 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 NJsonSchema
, I created a reliable contract between my TypeScript and C# 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.