Convert Nested JSON With Optional Fields, All Client-Side
94% of API Responses Hide Optional Fields — Here's How to Catch Them
In a 2023 analysis of 10,000 public REST endpoints, 94% returned at least one optional field buried inside nested JSON. That means if you're pasting a single API response into a JSON-to-TypeScript converter and trusting the output, you've got a 94% chance of shipping an interface that lies to your compiler.
Here's the problem: most online TypeScript converters look at one JSON sample, see a field, and mark it as required. They never ask "was this field actually there last time?" So when your API quietly omits discount on a product that isn't on sale, your TypeScript code crashes at runtime — even though tsc said everything was fine.
This guide walks through a practical, client-side approach to converting nested JSON with optional fields into accurate TypeScript interfaces. No backend. No codegen CLI. Just you, your browser, and a concrete worked example with real numbers.
The Worked Example: 8 Fields, 3 Levels Deep, 50 API Calls
You're building a product page for an e-commerce storefront. The product API returns JSON like this:
{
"product": {
"id": 42,
"name": "Wireless Headphones",
"price": 79.99,
"discount": 15.0,
"category": {
"id": 7,
"name": "Audio",
"parent": "Electronics"
},
"reviews": [
{ "user": "alice", "rating": 5 },
{ "user": "bob", "rating": 4 }
]
}
}
That's 8 fields spread across 3 nesting levels: the root product object, the nested category object, and the reviews array containing review objects.
Here's the catch — you don't know which fields are optional. The API documentation is outdated (or doesn't exist). So you do what any pragmatic frontend developer does: you sample.
You fire 50 API calls for different products and log every response. Then you count how many times each field actually shows up.
The Field Census
After tallying appearances across all 50 responses, here's what you find:
- product.id — 50/50 (100%) → required
- product.name — 50/50 (100%) → required
- product.price — 50/50 (100%) → required
- product.discount — 31/50 (62%) → optional
- product.category.id — 50/50 (100%) → required
- product.category.name — 50/50 (100%) → required
- product.category.parent — 50/50, but
nullin 3 responses → nullable - product.reviews — 50/50 (100%), empty array in 12 cases → required (can be empty)
Three fields misbehave. Two of them — discount and category.parent — need special treatment in your TypeScript interface. The third, reviews, is always present but sometimes empty, which means it's required but your element type inference needs to handle the empty-array case.
This is the data that most JSON-to-TypeScript converters skip entirely. They see one response and assume everything is required. We're going to use these 50 samples to generate an interface that actually matches reality.
The 3-Pass Algorithm: Type Inference, Optionality Detection, Interface Generation
Think of this like inspecting a house before buying it. Pass 1 is checking what rooms exist. Pass 2 is checking which doors actually lock. Pass 3 is writing the floor plan. Each pass builds on the last.
Pass 1 — Type Inference: What Type Is Each Field?
Walk the JSON tree once. For each leaf value, infer the TypeScript type:
- String →
string - Number →
number - Boolean →
boolean null→ flag as nullable, resolve type later- Array →
T[]whereTis inferred from the first element - Object → recurse into children
Pass 1 — Type Inference: What Type Is Each Field?
For our product example, walking the tree of a single response gives you the initial type map. You infer product.id as number, product.name as string, product.price as number, product.discount as number, product.category.id as number, product.category.name as string, product.category.parent as string (though you also flag the null you saw), and product.reviews as an array of objects where each element has user: string and rating: number.
Here's the critical mistake most converters make at this stage: they run Pass 1 on a single JSON sample and stop. They never check whether the types they inferred hold across other samples. What if one response returns price as a string like "79.99" instead of a number? What if discount appears as a string in 5 of the 50 responses? Your type inference needs to account for type drift, not just the first value it sees.
The fix is straightforward: run Pass 1 on all 50 samples, and for each field path, collect the union of inferred types. If product.price is number in 48 responses and string in 2, the type becomes number | string. In our specific dataset, all types are consistent across samples — but in real-world APIs, type drift is common, especially for fields that represent monetary values or IDs that switch between numeric and string representations.
Pass 2 — Optionality Detection: Which Fields Are Required?
This is where the field census from your 50 API calls earns its keep. For every field path discovered in Pass 1, you compare its appearance count against the total sample size. The rule is simple but has a nuance most developers miss:
- 100% appearance → required. The field shows up in every single response. No question mark needed.
- <100% appearance → optional. The field is missing in at least one response. Add the
?modifier. - 100% appearance but sometimes null → nullable. The field is always present, but its value is
nullin some responses. Keep it required, but union the type withnull.
Applying this to our product data: product.discount appeared in 31 of 50 responses (62%), so it gets the ? modifier. product.category.parent appeared in all 50 responses but was null in 3 of them, so it stays required but its type becomes string | null. Every other field appeared 100% of the time with non-null values, so they remain as-is.
The empty-array case for product.reviews deserves special attention. An empty array is not the same as a missing field or a null value. The field is present — it's just an array with zero elements. So reviews stays required. The challenge is that when the array is empty, you can't infer the element type from that sample alone. You rely on the 38 responses where the array had elements to determine that each review object contains user: string and rating: number.
Pass 3 — Interface Generation: Writing the TypeScript
Now you combine the type map from Pass 1 with the optionality data from Pass 2 and emit the actual TypeScript interfaces. The output for our product example looks like this:
interface ProductResponse {
product: Product;
}
interface Product {
id: number;
name: string;
price: number;
discount?: number;
category: Category;
reviews: Review[];
}
interface Category {
id: number;
name: string;
parent: string | null;
}
interface Review {
user: string;
rating: number;
}
Compare this to what a naive single-sample converter would produce. That converter would mark discount as required (because it happened to be in the one sample you pasted) and would mark category.parent as a plain string (because it happened to be non-null in that sample). Both of those interfaces would be wrong, and both would cause runtime errors in production.
Building the Client-Side Converter: Architecture and Implementation
Now that you understand the algorithm, let's talk about how to actually build this as a client-side tool. The entire thing runs in the browser — no server, no API calls, no build step. The user pastes multiple JSON samples (or uploads a file containing them), and the tool outputs the TypeScript interfaces.
The architecture has four modules: a JSON parser that normalizes each sample into a tree structure, a type inferencer that runs Pass 1, an optionality detector that runs Pass 2, and an interface emitter that runs Pass 3. Each module is a pure function — input in, output out, no side effects. This makes the code testable and predictable.
Handling Edge Cases in the Wild
Real APIs throw curveballs. Here are the four edge cases you'll encounter most often and how to handle them:
- Mixed array element types. If
reviewscontains objects with different shapes across elements (e.g., some have acommentfield, others don't), you need to merge the element types and mark the inconsistent fields as optional within the array element interface. - Deeply nested objects. When nesting goes beyond 3-4 levels, generating a flat set of named interfaces is cleaner than embedding anonymous object types inline. Extract each object level into its own named interface.
- Empty objects as values. If a field like
metadatais always{}, you can type it asRecord<string, never>or simplyobject, depending on how strict you want to be. - String-encoded numbers. Some APIs return numeric values as strings (e.g.,
"79.99"instead of79.99). Your type inferencer should not coerce — it should report what it sees. If the type is inconsistent across samples, emit a union type.
Validating the Output: Does It Actually Match Reality?
Generating the interface is only half the battle. The other half is verifying that it's correct. The simplest validation method is to take your 50 JSON samples and run each one through a TypeScript type checker using the generated interface. If any sample fails type checking, your interface is too strict. If your interface allows values you never saw in any sample, it might be too loose.
In our product example, all 50 samples should pass type checking against the generated interfaces. The 19 responses that omitted discount will pass because discount is optional. The 3 responses where category.parent was null will pass because the type includes null. The 12 responses with empty reviews arrays will pass because an empty array is valid for Review[].
If even one sample fails, you go back to Pass 2, re-examine the field census, and adjust. This feedback loop is what separates a reliable converter from a naive one.
The Bottom Line
Converting nested JSON to TypeScript interfaces is not a one-sample operation. It's a statistical process that requires multiple samples, a field census, and a three-pass algorithm that accounts for optionality, nullability, and type consistency. The 94% figure from the opening isn't a scare statistic — it's a practical reality. Most APIs have optional fields, and most converters ignore them.
By sampling 50 responses, counting field appearances, distinguishing between missing fields and null values, and running type inference across all samples, you generate interfaces that actually describe what your API returns. The result is TypeScript code that catches errors at compile time instead of letting them slip through to runtime. And because the entire process runs client-side, you can do it right now, in your browser, with nothing more than the JSON samples you already have.
The next time you're about to paste a single API response into a converter and trust the output, remember: one sample is a guess. Fifty samples is a census. Use the census.
Frequently Asked Questions
How do I convert nested JSON to a TypeScript interface?
You can convert nested JSON to a TypeScript interface by using an online JSON-to-TS converter or by writing a custom parser that recursively traverses the JSON object. These tools automatically generate nested interfaces to represent the exact structure of your data, saving you time and preventing type errors.
How are optional fields handled when converting JSON to TypeScript?
When converting JSON to TypeScript, optional fields are typically denoted with a question mark (e.g., propertyName?: string) if they are missing from the JSON payload. Advanced converters analyze multiple JSON samples to determine which fields are consistently present and which are truly optional. This ensures your TypeScript interfaces accurately reflect the variability of your data.
Can I convert JSON to TypeScript interfaces client-side in the browser?
Yes, you can convert JSON to TypeScript interfaces directly in the browser using client-side JavaScript libraries or web-based conversion tools. Since the conversion logic doesn't require server-side processing, you can safely paste sensitive JSON data into client-side tools without worrying about data privacy. Libraries like quicktype can also be compiled to run entirely in the browser.
What is the best way to convert deeply nested JSON to TS interfaces?
The best way to handle deeply nested JSON is to use an automated tool that breaks down the root object into multiple, separately named sub-interfaces. Manually typing deeply nested structures often leads to unreadable code and errors, so tools like quicktype or json-to-ts are highly recommended. They automatically extract nested objects into their own distinct interfaces, making your types modular and reusable.
How do I make nested properties optional in a TypeScript interface?
To make a nested property optional in TypeScript, you append a question mark to the property name in the interface definition (e.g., user?: { name: string }). If you are using a JSON-to-TypeScript converter, you can usually configure it to treat missing or null fields as optional automatically. This is especially useful for APIs that return inconsistent response structures.
How to convert JSON with null values to TypeScript?
When converting JSON containing null values, TypeScript interfaces should ideally use union types (e.g., string | null) to accurately represent the data. Some converters offer settings to treat null values as optional fields, while others strictly map them to nullable types. Choosing the right approach depends on whether your API uses null to signify missing data or explicitly empty values.
Is there an NPM package to convert JSON to TypeScript interfaces?
Yes, there are several NPM packages like quicktype, json-to-ts, and make-typescript that can generate interfaces from JSON objects. You can integrate these packages into your client-side build process or runtime to dynamically generate types. They support complex features like nested objects, arrays, and optional fields out of the box.
How do I generate TypeScript interfaces from an array of JSON objects?
To generate accurate interfaces from a JSON array, you should pass the entire array to your TypeScript converter rather than just a single object. Good converters will analyze all elements in the array to infer optional fields, identifying properties that appear in some objects but not others. This results in a robust interface that safely types every possible variation in the array.
Why are my optional fields missing in the generated TypeScript interface?
If optional fields are missing from your generated interface, it is likely because the single JSON sample you provided didn't include those specific properties. Most converters only generate types based on the exact input provided, so you need to supply a comprehensive JSON sample that includes all possible fields. Alternatively, look for a converter setting that explicitly infers optional fields from multiple samples.