Free JSON Developer Tools
2026-02-23 · 11 min read · By AllJSONTools
Paste broken JSON and fix it instantly with AI — plain-English explanations included.
Every modern application depends on APIs. Whether you are fetching user profiles from a REST endpoint, querying a GraphQL server, or consuming data from a third-party service, the JSON that comes back over the wire is the foundation your application logic builds on. When that foundation is shaky — when you assume a field exists that doesn’t, or parse a response without catching errors — the entire application becomes fragile.
Fragile API response handling is one of the most common sources of runtime errors in production. A missing data field causes an “undefined is not an object” crash. An unexpected null value breaks a render pipeline. A malformed JSON string from a compromised endpoint introduces a security vulnerability. These are not edge cases; they are everyday realities that robust response handling prevents.
This guide covers practical strategies for parsing, validating, and consuming API responses in TypeScript and JavaScript applications. By the end, you will have a toolkit of patterns that make your API integrations safer, more predictable, and easier to debug.
Before diving into parsing and validation, it helps to recognize the response shapes you will encounter most often. Knowing the pattern lets you write targeted handling logic instead of one-size-fits-all code.
The most common pattern is a simple JSON object with the requested data at the top level or wrapped in a data envelope:
{
"data": {
"id": 42,
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "admin"
},
"meta": {
"requestId": "req_abc123",
"timestamp": "2025-01-15T10:30:00Z"
}
}GraphQL responses always follow a standard shape with data and optional errors fields. Crucially, a GraphQL response can contain both partial data and errors simultaneously:
{
"data": {
"user": {
"name": "Alice Johnson",
"posts": null
}
},
"errors": [
{
"message": "Failed to fetch posts",
"path": ["user", "posts"],
"extensions": { "code": "DOWNSTREAM_ERROR" }
}
]
}APIs returning lists typically paginate results. The response includes metadata about the current page, total count, and navigation cursors or page numbers:
{
"data": [
{ "id": 1, "title": "First Post" },
{ "id": 2, "title": "Second Post" }
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 158,
"totalPages": 8,
"nextCursor": "eyJpZCI6MjB9"
}
}Well-designed APIs return structured error objects that include a machine-readable code, a human-readable message, and optionally a list of field-level validation errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body failed validation",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
}The first step in handling any API response is parsing the raw text into a JavaScript object. This sounds trivial, but JSON.parse() throws an exception when the input is not valid JSON — and API responses are not always valid JSON. Network errors, proxy interference, HTML error pages from load balancers, and truncated responses can all produce unparseable strings.
Never call JSON.parse() without error handling. A bare call is a ticking time bomb in production:
// Unsafe — will crash on invalid JSON
const data = JSON.parse(responseBody);
// Safe — graceful error handling
function safeJsonParse<T>(text: string): { data: T | null; error: string | null } {
try {
const data = JSON.parse(text) as T;
return { data, error: null };
} catch (err) {
return {
data: null,
error: err instanceof SyntaxError ? err.message : "Unknown parse error",
};
}
}
// Usage
const result = safeJsonParse<UserResponse>(responseBody);
if (result.error) {
console.error("Failed to parse API response:", result.error);
return;
}
console.log(result.data);Several edge cases can surprise you. JSON.parse("null") returns null without throwing. JSON.parse("42") returns the number 42. Both are valid JSON but probably not the object you expected. Always verify the parsed result is the type you need:
function parseJsonObject(text: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(text);
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
return parsed as Record<string, unknown>;
} catch {
return null;
}
}For very large API responses, parsing the entire body into memory at once can cause performance problems. Modern runtimes support streaming JSON parsing. The Fetch API’s response.body gives you a ReadableStream that you can process incrementally:
async function streamJsonResponse(url: string): Promise<unknown[]> {
const response = await fetch(url);
if (!response.body) throw new Error("No response body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
const items: unknown[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete JSON lines (NDJSON / JSON Lines format)
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.trim()) {
items.push(JSON.parse(line));
}
}
}
return items;
}Parsing JSON tells you the string is syntactically valid. It does not tell you that the resulting object has the shape your application expects. A parsed response might be missing required fields, have fields with wrong types, or contain unexpected data. This is where validation comes in.
The simplest approach is to write a type guard function that checks the structure at runtime. TypeScript’s type narrowing ensures that after the guard succeeds, the compiler knows the exact type:
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as Record<string, unknown>).id === "number" &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string" &&
"email" in value &&
typeof (value as Record<string, unknown>).email === "string"
);
}
// Usage
const parsed = JSON.parse(responseText);
if (isUser(parsed)) {
// TypeScript knows `parsed` is User here
console.log(parsed.name.toUpperCase());
} else {
console.error("Invalid user response shape");
}This works for simple types but becomes tedious for complex nested objects. For anything beyond a few fields, a validation library is the better choice.
Zod is a TypeScript-first schema validation library that lets you define your expected response shape once and get both runtime validation and static type inference. It eliminates the need to write manual type guards:
import { z } from "zod";
// Define the expected response schema
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user", "moderator"]),
createdAt: z.string().datetime(),
});
// Infer the TypeScript type from the schema
type User = z.infer<typeof UserSchema>;
// Wrapper for the full API response
const ApiResponseSchema = z.object({
data: UserSchema,
meta: z.object({
requestId: z.string(),
timestamp: z.string().datetime(),
}),
});
// Parse and validate in one step
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const json = await response.json();
const result = ApiResponseSchema.safeParse(json);
if (!result.success) {
console.error("Validation failed:", result.error.flatten());
throw new Error("Invalid API response");
}
return result.data.data;
}If you already have sample JSON responses, you can generate Zod schemas automatically using our JSON to Zod Schema converter. Paste your API response and get a ready-to-use Zod schema in seconds.
For language-agnostic validation — especially when the schema is shared across teams or services — JSON Schema is the industry standard. Libraries like ajv provide high-performance validation in JavaScript:
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const userResponseSchema = {
type: "object",
properties: {
data: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" },
},
required: ["id", "name", "email"],
},
},
required: ["data"],
};
const validate = ajv.compile(userResponseSchema);
async function fetchAndValidate(url: string) {
const response = await fetch(url);
const json = await response.json();
if (!validate(json)) {
console.error("Schema violations:", validate.errors);
throw new Error("Response does not match expected schema");
}
return json.data;
}You can test your JSON data against any schema using our JSON Schema Validator tool, which highlights errors in real time.
Not every API call succeeds. Network failures, server errors, rate limits, and validation rejections are all normal parts of API communication. Your error handling strategy determines whether these failures degrade gracefully or crash your application.
Always check the HTTP status code before attempting to parse the response body. Here are the most important status codes to handle:
| Status | Meaning | Action |
|---|---|---|
200 | OK | Parse and validate the response body |
201 | Created | Parse the created resource from the body |
204 | No Content | Do not attempt to parse the body — it is empty |
400 | Bad Request | Parse error details and display to user |
401 | Unauthorized | Refresh token or redirect to login |
403 | Forbidden | Show permission denied message |
404 | Not Found | Show “resource not found” message |
429 | Too Many Requests | Back off and retry after the Retry-After header |
500 | Internal Server Error | Log the error and retry with exponential backoff |
503 | Service Unavailable | Retry after a delay; show maintenance message |
Wrapping fetch in a helper function that handles status codes, parsing, and errors in one place eliminates repetitive boilerplate across your codebase:
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
public body: unknown,
) {
super(`API Error ${status}: ${statusText}`);
this.name = "ApiError";
}
}
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, {
headers: { "Content-Type": "application/json", ...options?.headers },
...options,
});
// Handle no-content responses
if (response.status === 204) {
return undefined as T;
}
// Attempt to parse JSON body
let body: unknown;
try {
body = await response.json();
} catch {
throw new ApiError(response.status, response.statusText, null);
}
// Throw on error status codes
if (!response.ok) {
throw new ApiError(response.status, response.statusText, body);
}
return body as T;
}Transient errors (5xx responses, network timeouts) often resolve themselves. A retry strategy with exponential backoff handles these gracefully without overwhelming the server:
async function fetchWithRetry<T>(
url: string,
options?: RequestInit,
maxRetries = 3,
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await apiFetch<T>(url, options);
} catch (error) {
const isRetryable =
error instanceof ApiError &&
(error.status >= 500 || error.status === 429);
if (!isRetryable || attempt === maxRetries) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s...
const delay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 500;
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
throw new Error("Unreachable");
}The patterns above can be composed into a fully type-safe API client layer. The goal is to ensure that every API call in your application returns a known TypeScript type, validated at runtime, so that the compiler can catch misuse at build time and the validator catches unexpected responses at runtime.
Combining our fetch wrapper with Zod schemas creates a function where the return type is automatically inferred from the schema you pass in:
import { z } from "zod";
async function typedFetch<T extends z.ZodType>(
url: string,
schema: T,
options?: RequestInit,
): Promise<z.infer<T>> {
const response = await fetch(url, options);
if (!response.ok) {
throw new ApiError(response.status, response.statusText, await response.json());
}
const json = await response.json();
return schema.parse(json);
}
// Define schemas for your endpoints
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const UsersListSchema = z.object({
data: z.array(UserSchema),
pagination: z.object({
page: z.number(),
total: z.number(),
}),
});
// Usage — return type is automatically inferred
const user = await typedFetch("/api/users/1", UserSchema);
// typeof user = { id: number; name: string; email: string }
const list = await typedFetch("/api/users", UsersListSchema);
// typeof list = { data: User[]; pagination: { page: number; total: number } }For larger applications, encapsulate your endpoints in a class that centralizes base URL configuration, authentication headers, and schema definitions:
class ApiClient {
constructor(private baseUrl: string, private token: string) {}
private async request<T extends z.ZodType>(
path: string,
schema: T,
options?: RequestInit,
): Promise<z.infer<T>> {
const response = await fetch(`${this.baseUrl}${path}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
...options,
});
if (!response.ok) {
throw new ApiError(response.status, response.statusText, await response.json());
}
return schema.parse(await response.json());
}
getUser(id: number) {
return this.request(`/users/${id}`, UserSchema);
}
listUsers(page = 1) {
return this.request(`/users?page=${page}`, UsersListSchema);
}
createUser(data: { name: string; email: string }) {
return this.request("/users", UserSchema, {
method: "POST",
body: JSON.stringify(data),
});
}
}
// Usage
const api = new ApiClient("https://api.example.com", "your-token");
const user = await api.getUser(42); // Fully typed and validatedThorough testing of your API response handling ensures that your parsing and validation logic works correctly for both happy paths and failure modes.
Create fixture files with realistic API responses for your tests. Keep both successful and error responses as fixtures to test all code paths:
// fixtures/user-response.json — used in tests
const mockUserResponse = {
data: {
id: 1,
name: "Test User",
email: "test@example.com",
role: "user",
createdAt: "2025-01-01T00:00:00Z",
},
meta: { requestId: "test-123", timestamp: "2025-01-01T00:00:00Z" },
};
const mockErrorResponse = {
error: {
code: "NOT_FOUND",
message: "User not found",
details: [],
},
};Use vi.fn() (Vitest) or jest.fn() to mock fetch and test your response handling in isolation:
import { describe, it, expect, vi, beforeEach } from "vitest";
describe("fetchUser", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("parses a valid user response", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(mockUserResponse),
});
const user = await fetchUser(1);
expect(user).toEqual(mockUserResponse.data);
expect(user.name).toBe("Test User");
});
it("throws on invalid response structure", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ data: { id: "not-a-number" } }),
});
await expect(fetchUser(1)).rejects.toThrow("Invalid API response");
});
it("throws ApiError on 404", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
json: () => Promise.resolve(mockErrorResponse),
});
await expect(fetchUser(999)).rejects.toThrow("API Error 404");
});
});Contract tests verify that the real API still matches the schema your client expects. Run these against a staging environment to catch breaking changes before they reach production. You can use your Zod schemas or JSON Schemas directly as the contract:
import { describe, it, expect } from "vitest";
describe("API contract: GET /api/users/:id", () => {
it("response matches the UserResponse schema", async () => {
const response = await fetch("https://staging.api.example.com/users/1");
const json = await response.json();
const result = ApiResponseSchema.safeParse(json);
expect(result.success).toBe(true);
if (!result.success) {
console.error("Contract violations:", result.error.flatten());
}
});
});When debugging API responses, inspecting payload differences, or generating schemas from sample data, having the right tools at hand saves significant time. AllJSONTools provides a suite of free, browser-based utilities designed specifically for working with JSON:
JSON Formatter — Paste a raw API response and instantly pretty-print it with syntax highlighting. Essential for reading compact or minified JSON payloads during debugging.
JSON Diff — Compare two API responses side by side to identify exactly what changed. Invaluable when debugging why a previously working integration suddenly broke.
JSON Schema Validator — Validate API responses against a JSON Schema in real time. Paste your schema on one side and a response on the other to see instant validation results.
JSON Tree Viewer — Visualize complex nested responses as a collapsible tree structure. Helps you understand the shape of unfamiliar API payloads quickly.
JSON to Zod Schema — Paste a sample API response and generate a Zod validation schema automatically. Jump-start your type-safe API client in seconds.
JSON to TypeScript — Generate TypeScript interfaces from a JSON response payload. Copy the types directly into your project for immediate type safety.
All of these tools run entirely in your browser — no data is sent to any server. Whether you are prototyping a new integration, debugging a production issue, or setting up validation for a new endpoint, these utilities are designed to fit seamlessly into your API development workflow.
Robust API response handling is not a nice-to-have; it is a prerequisite for building reliable software. Start by wrapping your parsing in try/catch, adopt a validation library like Zod for runtime type checking, implement proper error handling with retry logic, and write tests that cover both successful and failure scenarios. These practices will save you countless hours of debugging and make your applications significantly more resilient.
Paste broken JSON and fix it instantly with AI — plain-English explanations included.
A practical guide to JSON Web Tokens — how they work, common security mistakes, and best practices for authentication. Includes code examples and debugging tips.
Learn what JSON Schema is, how it works, and how to use it for API validation, form generation, and data documentation. Includes examples and common patterns.
Master JSON API design with 12 proven best practices — response envelopes, naming conventions, pagination, error handling, versioning, security headers, and more. Includes code examples.
Everything you need to know about parsing JSON in JavaScript — JSON.parse, JSON.stringify, error handling, reviver functions, fetch API, TypeScript type safety with Zod, streaming large files, and common pitfalls. Includes AI-powered error fixing.