Back to HomeFORMAT//LABTYPESCRIPT FIELD GUIDE
A HANDS-ON FIELD GUIDE · 00—06

Data has
many dialects.

Learn how JSON, XML, and YAML shape the same information—and how to move between them with TypeScript.

Start exploring ↘
{ }
JSON<XML/>YAML
00 / START HERE

Why JSON exists.

A program’s data lives in memory, and memory is wiped the moment the program stops. Whenever data has to go anywhere else, it must first be written out as plain text. JSON is the agreed set of rules for writing that text, so whoever reads it gets back exactly what was sent.

01Remember it for next time

A program forgets everything when it closes. To keep a to-do list, your settings or a high score until tomorrow, it writes them to a file. Section 05 shows how.

02Send it across the internet

A weather app doesn’t store the forecast; a server does. The app asks, and the server answers with text like this:

{"city": "Chicago", "temp": 41, "rain": true}
03Hand it to another program

A Python script on a server and JavaScript in a browser can’t see each other’s memory, and they’re written in different languages. Both can read JSON.

Why not make up your own format?

You could save a person as one line of text, with commas between the fields. That works until a name contains a comma. Both programs below print with the same three lines; only the way they read the record differs.

HOMEMADE.TS · COMMAS
// A homemade format: fields separated by commas
const line = "Lovelace, Ada,programmer,true";

const [fullName, role, active] = line.split(",");
console.log(`name:   "${fullName}"`);
console.log(`role:   "${role}"`);
console.log(`active: ${active} (${typeof active})`);
$ node homemade.ts
name:   "Lovelace"
role:   " Ada"
active: programmer (string)

Wrong. The comma inside the name split it in two, so every field after it shifted. And even when nothing shifts, splitting text only ever gives back text: active can never be a real true or false.

JSON.TS · JSON
// The same record as JSON: quotes mark where text ends
const text =
  '{"name":"Lovelace, Ada","role":"programmer","active":true}';

const { name: fullName, role, active } = JSON.parse(text);
console.log(`name:   "${fullName}"`);
console.log(`role:   "${role}"`);
console.log(`active: ${active} (${typeof active})`);
$ node json.ts
name:   "Lovelace, Ada"
role:   "programmer"
active: true (boolean)

Right. The quotes show exactly where the name ends, and true comes back as a real true/false value, because JSON’s rules say which values are text and which aren’t.

That’s the whole need: data has to leave a program as text, and JSON is the text format every language already agrees on. JSON.stringify() turns data into that text and JSON.parse() turns it back, so nobody has to write, or debug, their own reader.

01 / THE FORMATS

One record. Three ways.

A person, expressed in three popular data formats.

01JSONAPIs · WEB APPS
{
  "name": "Ada Lovelace",
  "role": "programmer",
  "active": true
}

Compact, structured, and native to JavaScript. Keys and strings use double quotes.

02XMLDOCUMENTS · LEGACY
<person>
  <name>Ada Lovelace</name>
  <role>programmer</role>
  <active>true</active>
</person>

Element tags describe structure. Verbose, but supports attributes and schemas.

03YAMLCONFIG · DEVOPS
person:
  name: Ada Lovelace
  role: programmer
  active: true

Clean and human-friendly. Indentation carries meaning; spaces matter.

02 / TYPESCRIPT

Parse once.
Work with data.

TypeScript adds types to JavaScript so the shape of your data is clear before you use it.

JSON is built in: JSON.parse() reads text and JSON.stringify() writes it. XML and YAML usually need a parser package, such as fast-xml-parser or yaml.

// Describe the data shape
type Person = {
  name: string;
  role: string;
  active: boolean;
};

// Parse JSON, then use it
const person: Person = JSON.parse(input);
console.log(person.name);
03 / TRY IT

Translate the payload.

Edit the JSON, then convert it to XML or simple YAML.

04 / QUICK REFERENCE

Pick the right tool.

Choose based on the job, the ecosystem, and the people who maintain the data.

JSONWeb APIs, app dataFast, widely supported
XMLDocuments, integrationsRich structure, schemas
YAMLConfiguration filesReadable, supports comments
05 / SAVING DATA

A .json file
as storage.

Variables live in memory and vanish when a program exits. To keep data, TypeScript writes it to a file: JSON.stringify() on save, JSON.parse() on load.

TASKS.TS
// tasks.ts: save and load a list with a plain .json file
import { existsSync, readFileSync, writeFileSync } from "node:fs";

// Describe the shape of one record
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const FILE = "tasks.json";

// Load: read the file's text, then JSON.parse it
function load(): Task[] {
  if (!existsSync(FILE)) return []; // first run: no file yet
  return JSON.parse(readFileSync(FILE, "utf8")) as Task[];
}

// Save: JSON.stringify the whole list and write it back
function save(tasks: Task[]): void {
  writeFileSync(FILE, JSON.stringify(tasks, null, 2) + "\n");
}

const tasks = load();
tasks.push({
  id: tasks.length + 1,
  title: process.argv[2] ?? "Untitled",
  done: false,
});
save(tasks);

console.log(`${tasks.length} task(s) saved in ${FILE}:`);
for (const t of tasks) console.log(`  #${t.id} ${t.title}`);
TERMINAL · RUN IT THREE TIMES
$ node tasks.ts "Learn JSON"
1 task(s) saved in tasks.json:
  #1 Learn JSON
$ node tasks.ts "Learn YAML"
2 task(s) saved in tasks.json:
  #1 Learn JSON
  #2 Learn YAML
$ rm tasks.json
$ node tasks.ts "Start over"
1 task(s) saved in tasks.json:
  #1 Start over
TASKS.JSON · AFTER THE FIRST TWO RUNS
[
  {
    "id": 1,
    "title": "Learn JSON",
    "done": false
  },
  {
    "id": 2,
    "title": "Learn YAML",
    "done": false
  }
]

Recent Node runs a .ts file directly (this output is from Node 22.23). If an older Node refuses it, run npx tsx tasks.ts instead.

What makes a .json file handy?

01Every language reads it

JSON.parse() is built into JavaScript and TypeScript, and Python, Go, PHP and C# ship JSON in their standard libraries. Java, C and Rust need one small library.

02You can read it

Open it in any text editor and see exactly what was saved. No special viewer, no binary format to decode.

03Nothing to set up

No server, no install, no schema. It’s one file you can copy, email or back up like any other.

04Git-friendly

It’s plain text, so git shows exactly which lines changed. That’s why tasks.ts saves with JSON.stringify(tasks, null, 2): one value per line.

05The web’s own format

Most web APIs send and receive JSON, so a reply can be saved to disk as-is and loaded again later.

06Fits TypeScript types

Objects, arrays, strings, numbers, booleans and null map straight onto a type like Task. Dates don’t: they come back as strings.

Good examples

FILES YOU’LL MEET
  • package.jsonEvery Node and TypeScript project: its name, scripts and dependencies.
  • tsconfig.jsonTypeScript’s compiler settings. It also allows comments, a relaxed variant called JSONC.
  • package-lock.jsonThe exact version of every installed package. npm writes it; you don’t.
  • .vscode/settings.jsonVS Code editor preferences, for you or for one project.
  • composer.jsonA PHP project’s dependencies, read by Composer.
  • manifest.jsonA browser extension’s name, version and permissions.
FILES YOU MIGHT WRITE
  • settings.jsonTheme, language, window size: a small object read at startup.
  • save-game.jsonHigh scores, a game’s progress, or a to-do list like tasks.json.
  • cache.jsonA slow API’s answer, saved so the next run reads the file instead of calling again.
  • fixtures.jsonSample records your tests load, so every run starts from the same data.

Does it store forever?

As long as the file does. Close the program, reboot the machine, come back next year: tasks.json is still on disk. The third run above only started over because the file was deleted. TypeScript itself stores nothing; its types disappear when the code runs, and every variable dies with the program.

So a file persists like a database does, but it isn’t one. Every save rewrites the whole file, every load reads all of it, and as Task[] is a promise to the compiler, not a check: a hand-edited file with a missing field or a wrong type loads without complaint. Fine for settings and small lists. When the data grows, or more than one user writes to it, move to a database such as SQLite.

.JSON FILEDATABASE
Survives a restartYes, until the file is deletedYes
Each saveRewrites the whole fileChanges only the rows touched
Find one recordLoad everything, then searchQuery it with SELECT … WHERE
Crash mid-saveCan leave a half-written fileRolls back to the last good state
Two writers at onceLast save wins; the other is lostWrites take turns; none is silently lost
Checks the shapeNo, not at runtimeConstraints like NOT NULL and CHECK
06 / GO + TYPESCRIPT

Two languages,
one JSON.

A common setup: a Go server holds the data and a TypeScript program uses it. They share no memory and no code. Everything that passes between them is JSON text, and each side describes the same shape in its own language.

The contract

Three descriptions of the same record. The json:"…" tags are what connect Go’s capitalized field names to the lowercase keys TypeScript expects.

JSON KEYGO STRUCT FIELDTYPESCRIPT TYPE
"id"ID int `json:"id"`id: number;
"title"Title string `json:"title"`title: string;
"done"Done bool `json:"done"`done: boolean;
MAIN.GO · THE GO SERVER
// main.go: a tiny Go API that speaks JSON
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"sync"
)

// Task matches the TypeScript type field for field.
// The `json:"..."` tags set the key names in the JSON.
type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

var (
	mu    sync.Mutex
	tasks = []Task{
		{ID: 1, Title: "Learn JSON", Done: true},
	}
)

func main() {
	// GET /tasks: Go data -> JSON text
	http.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(tasks)
	})

	// POST /tasks: JSON text -> Go data
	http.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
		var t Task
		if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
			http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
			return
		}
		mu.Lock()
		t.ID = len(tasks) + 1
		tasks = append(tasks, t)
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(t)
	})

	fmt.Println("listening on http://localhost:8080")
	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
CLIENT.TS · THE TYPESCRIPT CLIENT
// client.ts: TypeScript talking to the Go API with JSON

// The same shape as the Go struct
type Task = {
  id: number;
  title: string;
  done: boolean;
};

const API = "http://localhost:8080/tasks";

async function main(): Promise<void> {
  // Send: TypeScript data -> JSON text -> Go
  const res = await fetch(API, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title: "Learn Go", done: false }),
  });
  if (!res.ok) throw new Error(await res.text());
  const created: Task = await res.json();
  console.log(`Go saved #${created.id}: ${created.title}`);

  // Receive: Go -> JSON text -> TypeScript data
  const tasks: Task[] = await (await fetch(API)).json();
  for (const t of tasks) {
    console.log(`  [${t.done ? "x" : " "}] #${t.id} ${t.title}`);
  }
}

main();
TERMINAL 1 · START THE SERVER
$ go run main.go
listening on http://localhost:8080
TERMINAL 2 · RUN THE CLIENT, THEN PEEK AT THE RAW JSON
$ node client.ts
Go saved #2: Learn Go
  [x] #1 Learn JSON
  [ ] #2 Learn Go
$ curl -s http://localhost:8080/tasks
[{"id":1,"title":"Learn JSON","done":true},{"id":2,"title":"Learn Go","done":false}]

Leave the server running in one terminal and run the client in a second. The "GET /tasks" route patterns need Go 1.22 or newer; this output is from Go 1.26 and Node 22.23. New to Go? See Install Go.

How the JSON travels

01TypeScript → JSON

JSON.stringify() turns { title: "Learn Go", done: false } into text, and fetch sends it to Go.

02JSON → Go

json.NewDecoder(r.Body).Decode(&t) reads that text into a Task struct, matching keys by the json tags.

03Go → JSON

json.NewEncoder(w).Encode(tasks) writes the whole list back out as text: the line curl shows above.

04JSON → TypeScript

res.json() parses the text into a Task[], so the editor knows every task has id, title and done.

Gotchas

  • done boolLowercase fields are private in Go, and encoding/json silently leaves them out. The JSON would have no done at all.
  • ID int (no tag)Without json:"…" tags Go writes the keys as ID, Title and Done, so t.id in TypeScript is undefined. Reading JSON in Go ignores case, which hides the mistake in the other direction.
  • {"titel": "x"}Go ignores unknown keys and fills missing ones with zero values: an empty title, and no error. Call DisallowUnknownFields() on the decoder to reject it.
  • const tasks: Task[]TypeScript doesn’t check JSON at runtime. If the Go struct changes, change the TypeScript type with it.

The Go struct and the TypeScript type are two descriptions of the same JSON. As long as they agree, either side can be rewritten, in any language, without the other noticing.

CHECK YOUR UNDERSTANDING

Three quick questions.