An application receives a customer record from an API and treats it as a TypeScript object. The editor offers property completion, the project compiles, and a developer can navigate directly to the type definition. At runtime, the customer identifier arrives as a number where the application expects a string. The type declaration has not changed what crossed the network.
TypeScript types describe and check relationships in program code. JSON Schema describes constraints that a runtime validator can apply to a JSON value. They can represent overlapping expectations, but they operate at different points and express different kinds of detail.
Using them together is often useful: validate external data before accepting it into the application’s typed workflow. Doing that reliably requires understanding what each check establishes and where a valid-looking object still needs business scrutiny.
A type assertion does not inspect the response
Consider an application type for a user record:
export type User = {
id: string;
displayName: string;
nickname?: string;
};
Within checked code, TypeScript can report incompatible assignments and invalid property use. That helps developers maintain assumptions as the program changes. The compiler’s settings and the way code uses features such as any influence how much checking occurs.
An assertion at an input boundary is different:
const raw = '{"id":42,"displayName":null}';
const user = JSON.parse(raw) as User;
The assertion tells the compiler to treat the result as User. It does not convert 42 to a string or reject the null display name. TypeScript’s handbook explains that type assertions are removed during compilation and provide no runtime checking.
Keeping the parsed value as unknown makes the unresolved boundary visible:
const received: unknown = JSON.parse(raw);
Code must then obtain evidence about that value before using it as a user record. That evidence can come from a runtime validator or carefully written checks. JSON parsing itself establishes that the text was valid JSON, not that it represents the application object.
A schema makes selected runtime constraints explicit
Here is a Draft 7 JSON Schema for a wire representation of the user. The dialect is declared so the validator can interpret the contract under the intended specification.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": { "type": "string", "minLength": 1 },
"displayName": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"nickname": { "type": "string" }
},
"required": ["id", "displayName"],
"additionalProperties": false
}
A validator configured for this dialect can check the declared conditions. It should reject the earlier numeric identifier and null display name. It should also reject an empty display name, a missing required identifier, or an unexpected property. The schema permits the nickname to be absent, but does not permit it to be null.
The type and schema intentionally do not express identical detail. The TypeScript type does not encode the string length restrictions. TypeScript also uses structural compatibility rules; its object type is not a general runtime instruction to reject every additional property. Do not describe additionalProperties: false as an automatic translation of an ordinary TypeScript object type.
JSON Schema’s object reference explains that listing a property does not make it required and that additional properties are allowed unless constrained. Those defaults matter when reviewing a schema: a document can look detailed while accepting considerably more than its author intended.
Decide what happens after a validation result
The simplest boundary rejects data that fails the contract and passes accepted data to the next stage. Keep the validation result connected to the exact value checked. Validating one response and then using another, or mutating the object after validation, can invalidate the evidence.
A typed validator or a well-founded type guard can let the compiler recognise the accepted value’s shape. Its type declaration must accurately reflect its runtime behaviour. A function declared to return a user record but implemented as an unchecked assertion merely hides the original problem behind a reassuring name.
Preserve a useful rejection category without returning unrestricted internal details to callers. A malformed object may require a client correction, while an upstream provider sending an incompatible payload may require an integration incident. The same validation error can belong to different operational workflows depending on the boundary.
Parsing and validation failures should also remain distinct. Invalid JSON never reaches schema validation as a JSON instance. A valid JSON array can parse successfully and still fail a schema that requires an object. Recording those stages helps identify whether the problem concerns transport, syntax, or the expected representation.
Validator behaviour is part of the contract
Declaring a schema is insufficient if the running validator interprets it differently from the author. Select a supported dialect, compile the schema during application setup, and treat schema compilation errors as configuration failures. Avoid silently falling back to weaker validation when a feature is unsupported.
Format checks are a common source of misplaced confidence. Depending on the dialect and configuration, format can be annotation rather than an enforced assertion. Libraries may require additional support for particular formats; Ajv’s format documentation describes its format handling. Verify the configured behaviour with representative accepted and rejected values.
Likewise, distinguish validation from transformation. A validator may offer options to coerce values, insert defaults, or remove additional fields. Those options change the data-processing policy. If the application accepts a numeric identifier by converting it to a string, document that decision rather than reporting that the original input already satisfied a string-only contract.
For a public API, silent removal of unknown properties can hide a caller’s misspelled field. For an ingestion workflow, a deliberate normalisation stage may be useful. Keep those choices explicit and test the resulting value as well as the acceptance result. The right behaviour depends on how callers discover and correct mistakes.
The wire model may differ from the application model
A JSON representation has a narrower set of values than a typical application object. Dates commonly cross a JSON boundary as strings and become date objects only after interpretation. Class methods and object prototypes are not carried by a JSON document. An optional property can be absent, while a present null is a separate value.
Numeric handling also deserves care. If an identifier can exceed the exact integer range used by the application runtime, modelling it as an unrestricted JSON number can lose information before later business checks. A documented string representation may better preserve its identity. Choose the representation based on the data’s meaning rather than the appearance of a sample response.
| Concern | TypeScript contributes | JSON Schema contributes |
|---|---|---|
| Property use in application code | Compile-time checking under configured rules | No direct check of program logic |
| External JSON shape | Requires runtime evidence at the boundary | Declared instance constraints |
| String length | Usually remains a runtime concern | Explicit length constraints |
| Extra object properties | Structural type rules and contextual checks | Explicit additional-property policy |
| Current account ownership | Application logic and authoritative state | No independent access decision |
For the user example, a well-formed identifier does not establish that the user exists. A valid display name does not establish that the authenticated caller may change it. The boundary design in buffering core systems from model output applies equally to ordinary API clients: structural acceptance precedes the relevant business and permission checks.
Keep representations aligned as they evolve
Maintaining a schema and a type by hand can work for a small interface, but changes need coordinated review. Adding a nullable field to one representation while leaving the other string-only creates disagreement. The error may remain hidden until the first null reaches production.
Generating types from a schema, or schemas from an appropriate source model, can reduce duplication. Inspect the generator’s supported features and the output it produces. A code generator cannot necessarily preserve every refinement, runtime transformation, or application-specific constraint. Choose a maintained source for the wire contract and make any additional internal constraints explicit.
Use fixtures that target the contract’s important edges: a valid user, a missing identifier, a numeric identifier, an empty name, a null nickname, and an extra property. Run them through the actual configured validator. Separately check that application code cannot use an unvalidated unknown as a user. These checks examine different guarantees rather than repeating the same assertion twice.
Compatibility needs review too. Adding a response field can break a consumer that rejects additional properties. Making an optional field required can break existing callers. Contract tests and integration tests help establish which consumers and real connections remain compatible with the proposed change.
The useful outcome is a boundary that can be explained: this value was parsed, these constraints were checked by this validator configuration, and these further rules were applied before use. TypeScript then helps preserve the accepted assumptions through the code. Each stage supplies specific evidence, making failures easier to locate and changes easier to assess.





