Build your own airport
The custom airport .json format — documented field by field, with a ready-to-fly sample.
The ATC Radar Simulator lets you add your own airports: open the simulator, click Start, and pick “⬆ Upload airport (.json)…” at the top of the airport list. Uploaded airports are checked against the format below, stored in your browser, listed under “Custom airports”, and can be removed at any time. They stay on your device — nothing is uploaded to a server.
⬇ Download the sample airport ⬇ Download the JSON Schema ▶ Open the simulator
Two fast ways to build your airport: by hand — download the sample, rename it and replace the coordinates and names with your airport's real data (from your country's AIP or another public source); or with an AI assistant — copy the ready-made prompt below into ChatGPT, Claude, Gemini or any other assistant and let it draft the file for you. The format is also published as a machine-readable JSON Schema for validators and tools.
The complete sample
This file passes validation as-is — upload it unchanged to see “Sample Intl” appear in your list:
{
"id": "XSMP",
"name": "Sample Intl",
"icao": "XSMP",
"iata": "SMP",
"country": "Atlantis",
"transitionLevel": 60,
"upperLimit": 130,
"nextSector": "Sample Control",
"ref": { "lat": 30.0, "lon": 40.0 },
"runways": [
{
"name": "09/27",
"ends": [
{ "id": "09", "ils": 12, "approach": "ILS", "default": true, "lat": 30.0, "lon": 39.981 },
{ "id": "27", "ils": 12, "approach": "ILS", "lat": 30.0, "lon": 40.019 }
]
},
{
"name": "18/36",
"ends": [
{ "id": "18", "ils": 10, "approach": "RNP", "lat": 30.0165, "lon": 40.0 },
{ "id": "36", "ils": 10, "approach": "RNP", "lat": 29.9835, "lon": 40.0 }
]
}
],
"waypoints": [
{ "name": "SMP", "kind": "vor", "lat": 30.0, "lon": 40.0 },
{ "name": "SB", "kind": "ndb", "lat": 30.0, "lon": 39.9 },
{ "name": "NORTO", "kind": "fix", "lat": 30.6, "lon": 40.0 },
{ "name": "SOUTA", "kind": "fix", "lat": 29.5, "lon": 40.0 },
{ "name": "WESTO", "kind": "fix", "lat": 30.0, "lon": 39.2 },
{ "name": "EASTA", "kind": "fix", "lat": 30.0, "lon": 40.8 },
{ "name": "OKTAN", "kind": "fix", "lat": 30.35, "lon": 40.45 },
{ "name": "DELMA", "kind": "fix", "lat": 29.65, "lon": 39.55 }
],
"airways": [
{ "name": "A100", "fixes": ["WESTO", "SMP", "EASTA"] },
{ "name": "B200", "fixes": ["NORTO", "SMP", "SOUTA"] }
],
"procedures": [
{ "name": "NORTO1A", "kind": "STAR", "fixes": ["NORTO", "SMP"] },
{ "name": "WESTO1A", "kind": "STAR", "fixes": ["WESTO", "SB", "SMP"] },
{ "name": "SOUTA1A", "kind": "STAR", "fixes": ["SOUTA", "SMP"] },
{ "name": "EASTA1D", "kind": "SID", "fixes": ["EASTA"] },
{ "name": "OKTAN1D", "kind": "SID", "fixes": ["OKTAN"] },
{ "name": "DELMA1D", "kind": "SID", "fixes": ["DELMA"] }
],
"overflightRoutes": [
["WESTO", "SMP", "EASTA"],
["NORTO", "SMP", "DELMA"]
],
"tmaRadius": 60
}
Top-level fields
| Field | Type | Meaning |
|---|---|---|
| id | string, required | Unique identifier, normally the ICAO code (e.g. OMDB). Must not clash with a built-in airport. |
| name | string, required | Display name shown in the airport list and on the radar. |
| icao | string, required | ICAO location indicator, drawn next to the field on the scope. |
| iata | string, optional | IATA code (e.g. DXB). |
| country | string, optional | Country name. If omitted it is derived from the ICAO prefix. |
| transitionLevel | number, optional | Transition level as a flight level (e.g. 60 = FL60). Below it, clearances are phrased as altitudes in feet; at or above, as flight levels. Default 60. |
| upperLimit | number, optional | Upper limit of your airspace (FL). Overflights cross below it, and departures’ planned “ATC FL” is at or just under it. |
| nextSector | string, optional | The facility departures and overflights are handed to (e.g. "Dubai Control"). |
| ref | object, required | { "lat", "lon" } in decimal degrees — the airport reference point, the centre of the radar. South and west are negative. |
| runways | array, required | At least one runway — see below. |
| waypoints | array, required | At least one named point — see below. |
| procedures | array, required | The SIDs and STARs — at least one of each. |
| airways | array, optional | Airway lines drawn on the map. |
| overflightRoutes | array, optional | Fix-name lists that overflying traffic follows across your airspace. |
| tmaRadius | number, optional | Radius (NM) of the circular TMA boundary drawn around the field. |
Runways
Each runway has a name (e.g. "09/27") and an ends array with both ends:
| Field | Type | Meaning |
|---|---|---|
| id | string, required | The designator, two digits plus optional L/C/R — "27", "31L". The landing course is derived from it (31 → 310°), so name runways by their real heading. |
| lat / lon | number, required | The threshold position of this end, decimal degrees. |
| ils | number, required | Length (NM) of the final-approach course drawn on the scope — typically 10–12. Use 0 for none. |
| approach | string, optional | Approach type label: "ILS", "RNP", "VOR"… ILS arrivals call “established localizer”; others call “established final”. |
| default | boolean, optional | Marks the runway preselected in the New Session dialog. |
Waypoints
| Field | Type | Meaning |
|---|---|---|
| name | string, required | The fix name shown on the map and used by procedures — usually 2–5 letters (e.g. WESTO). Names of 3 letters or fewer are spelled phonetically on the radio. |
| kind | string, required | "fix" (triangle), "vor" (hexagon) or "ndb" (circle). |
| lat / lon | number, required | Position in decimal degrees. |
Procedures — SIDs and STARs
Every procedure is { "name", "kind", "fixes" }, where kind is "SID" or "STAR" and fixes is an ordered list of waypoint names. You need at least one STAR and one SID — arrivals spawn on a random STAR and departures are assigned a random SID.
- STARs run from an entry fix toward the field. End them at (or near) an on-field navaid so the arrival route connects to the airport — arrivals fly the listed fixes, then you vector them to final.
- SIDs are shown on the departure strip as the exit; departures fly runway heading until you clear them direct to a fix. The last fix of the SID is treated as the exit point.
Airways and overflight routes
airways draw faint route lines on the map: { "name": "A100", "fixes": ["WESTO", "SMP", "EASTA"] }. overflightRoutes are plain arrays of fix names, e.g. ["WESTO", "SMP", "EASTA"] — overflying traffic picks one and crosses your airspace along it. Every referenced fix must exist in waypoints.
How the simulator uses your data
Knowing what the engine does with each field makes a better airport:
- Landing course is derived from the runway end designator × 10 (
"31L"→ 310°) — so use the real painted designators and real threshold coordinates. - Arrivals spawn on a random STAR, 2–16 NM beyond its first fix, descending. They fly the listed fixes in order, then maintain heading — you vector them to final. That's why a STAR should end at (or within ~5 NM of) the field: the route visually and practically connects to the airport.
- Departures roll from the active runway climbing on runway heading; their SID is shown on the strip and its last fix is the exit point. Their planned "ATC FL" is the
upperLimitrounded down to a whole flight level. - Overflights pick a random entry-to-exit route from
overflightRoutesand cross belowupperLimit. - The active runway is chosen by best headwind unless the player picks one;
"default": truepreselects a runway in the dialog. - Radio phraseology: below
transitionLevelclearances are spoken as altitudes in feet, at/above as flight levels; waypoint names of three letters or fewer are spelled phonetically ("SMP" → "Sierra Mike Papa"). - Storage: uploaded airports live only in your browser (localStorage). Re-uploading the same
idreplaces the previous version, and you can remove one any time from the same dialog.
What the upload check requires
- Valid JSON, with
id,name,icaoandrefcoordinates in range. - At least one runway whose ends have valid designators, positions and an
ilsnumber. - At least one waypoint, each with a valid
kindand position. - At least one STAR and one SID, and every fix referenced anywhere must exist in
waypoints. - An
idthat isn't already used by a built-in airport. Re-uploading with the same id replaces your previous custom version.
If something is wrong, the upload alert tells you exactly which field to fix:
| Alert says… | How to fix it |
|---|---|
| "This is not a valid JSON file" | The file isn't parseable JSON — check for trailing commas, comments or missing quotes (JSON allows none of these). |
| missing "id" / "name" / "icao" / "ref" | Add the field at the top level; ref needs numeric lat and lon. |
| runway …: needs an "ends" array with both ends | Each runway must list both thresholds, e.g. ends for "13" and "31". |
| bad end id "…" | Runway end ids are two digits plus optional L/C/R — "09", "31L". |
| runway end …: needs "ils" | Add "ils" (final course length in NM) to every end — use 0 if there is no instrument final. |
| waypoint …: "kind" must be… | Use exactly "fix", "vor" or "ndb" (lower-case). |
| needs at least one STAR / SID | Add at least one procedure of each kind — arrivals need a STAR, departures a SID. |
| procedure / airway / route: unknown fix "…" | Every name in a fixes list must exactly match a waypoints[].name (case-sensitive). |
| "…" is already a built-in airport | Pick a different id — you can't override the built-in set. |
Machine-readable schema
The full format is published as a JSON Schema (draft-07) at /airport-schema.json — use it with any schema validator, editor tooling or AI agent. The sample above validates against it. Cross-field rules a schema can't express (fix references, built-in id collisions, "STARs end at the field") are noted in its description fields and enforced by the upload check.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://atcradarsimulator.com/airport-schema.json",
"title": "ATC Radar Simulator — custom airport file",
"description": "A custom airport for the free browser game at https://atcradarsimulator.com. Upload via New Session → '⬆ Upload airport (.json)…'. All coordinates are decimal degrees (south and west negative). Cross-field rules the schema cannot express: (1) every fix name referenced by procedures, airways or overflightRoutes must exist in waypoints[].name; (2) 'id' must not equal a built-in airport id (see https://atcradarsimulator.com/airports.html); (3) STARs should end at, or within ~5 NM of, 'ref' so arrival routes connect to the airport.",
"type": "object",
"required": ["id", "name", "icao", "ref", "runways", "waypoints", "procedures"],
"properties": {
"id": {
"type": "string",
"minLength": 2,
"description": "Unique identifier, normally the ICAO code, e.g. \"OMDB\". Re-uploading the same id replaces the previous custom airport."
},
"name": { "type": "string", "minLength": 1, "description": "Display name, e.g. \"Dubai Intl\"." },
"icao": { "type": "string", "minLength": 2, "description": "ICAO location indicator, drawn next to the field on the radar." },
"iata": { "type": "string", "description": "Optional IATA code, e.g. \"DXB\"." },
"country": { "type": "string", "description": "Optional country name; derived from the ICAO prefix when omitted." },
"transitionLevel": {
"type": "number",
"description": "Transition level as a flight level number, e.g. 60 = FL060. Below it clearances are phrased as altitudes in feet on QNH; at or above as flight levels. Default 60. Use 180 for US airports."
},
"upperLimit": {
"type": "number",
"description": "Upper limit of the terminal airspace as a flight level, e.g. 130. Overflights cross below it and departures' planned exit level ('ATC FL') is at or just under it."
},
"nextSector": { "type": "string", "description": "Facility that departures/overflights are handed to, e.g. \"Dubai Control\"." },
"ref": {
"type": "object",
"required": ["lat", "lon"],
"properties": {
"lat": { "type": "number", "minimum": -90, "maximum": 90 },
"lon": { "type": "number", "minimum": -180, "maximum": 180 }
},
"description": "Airport reference point — the centre of the radar picture."
},
"runways": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "ends"],
"properties": {
"name": { "type": "string", "description": "e.g. \"13/31\"" },
"ends": {
"type": "array",
"minItems": 2,
"items": {
"type": "object",
"required": ["id", "lat", "lon", "ils"],
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9]{2}[LCR]?$",
"description": "Painted designator, e.g. \"31L\". The landing course is derived from it (31 → 310°), so use the real designator."
},
"lat": { "type": "number", "minimum": -90, "maximum": 90, "description": "Threshold latitude." },
"lon": { "type": "number", "minimum": -180, "maximum": 180, "description": "Threshold longitude." },
"ils": { "type": "number", "minimum": 0, "description": "Final-approach course length drawn on the scope, NM (typically 10–12; 0 = none)." },
"approach": { "type": "string", "description": "Approach label: \"ILS\", \"RNP\", \"VOR\"… ILS arrivals report 'established localizer'; others 'established final'." },
"default": { "type": "boolean", "description": "Preselects this runway in the New Session dialog." }
}
}
}
}
}
},
"waypoints": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "kind", "lat", "lon"],
"properties": {
"name": { "type": "string", "minLength": 1, "description": "Fix name, usually 2–5 letters, e.g. \"LORNI\". Names of ≤3 letters are spelled phonetically on the radio." },
"kind": { "enum": ["fix", "vor", "ndb"], "description": "Symbol: fix = triangle, vor = hexagon, ndb = circle." },
"lat": { "type": "number", "minimum": -90, "maximum": 90 },
"lon": { "type": "number", "minimum": -180, "maximum": 180 }
}
}
},
"procedures": {
"type": "array",
"minItems": 2,
"description": "Needs at least one STAR (arrivals spawn on a random STAR) and one SID (assigned to departures). Every entry in fixes must match a waypoints[].name exactly.",
"items": {
"type": "object",
"required": ["name", "kind", "fixes"],
"properties": {
"name": { "type": "string", "description": "Procedure name, e.g. \"LORNI1A\"." },
"kind": { "enum": ["SID", "STAR"] },
"fixes": { "type": "array", "minItems": 1, "items": { "type": "string" } }
}
},
"allOf": [
{ "contains": { "properties": { "kind": { "const": "STAR" } }, "required": ["kind"] } },
{ "contains": { "properties": { "kind": { "const": "SID" } }, "required": ["kind"] } }
]
},
"airways": {
"type": "array",
"description": "Optional faint route lines on the map.",
"items": {
"type": "object",
"required": ["name", "fixes"],
"properties": {
"name": { "type": "string" },
"fixes": { "type": "array", "minItems": 2, "items": { "type": "string" } }
}
}
},
"overflightRoutes": {
"type": "array",
"description": "Optional crossing routes for overflying traffic: each an ordered list of waypoint names, entry → exit.",
"items": { "type": "array", "minItems": 2, "items": { "type": "string" } }
},
"tmaRadius": { "type": "number", "description": "Radius (NM) of the circular TMA boundary drawn around the field — roughly the farthest fix plus a small margin." }
}
}
Generate an airport with AI
Copy this prompt into any AI assistant (ChatGPT, Claude, Gemini…), put your airport's name in the TASK line, and it will answer with just the file. The prompt deliberately frames the request as a casual game level — approximate data is welcome — so the assistant doesn't wrap the answer in accuracy or licensing commentary. Save the reply as yourairport.json and upload it; the checker will point out anything the AI got wrong.
Tip: for the most realistic result, paste any runway/fix data you have under the TASK line — the assistant will use it. If the assistant answers as plain text instead of a file, use the converter just below the prompt.
I am making a level for a casual browser game (a radar traffic game). Levels are small JSON files describing a fictional-friendly version of an airport. This is entertainment only — nothing here is used for real navigation — so approximate values from your general knowledge are perfectly fine, and invented (but realistic-looking) names are fine too.
TASK
Write ONE JSON object for the airport: <AIRPORT NAME / ICAO>
HOW TO ANSWER
- If your platform can create downloadable files, save the JSON as a file named after the ICAO code (for example omdb.json) and give it to me as a download. Otherwise reply with the JSON object as plain text.
- Either way: JSON ONLY. No introduction, no explanations, no notes, no warnings, no markdown code fences.
- Do not comment on accuracy, data sources or licensing — none of that applies to a game level.
- Unsure of an exact coordinate? Give your best approximation. Don't recall real fix names? Invent realistic five-letter ones (like "TOLBA", "REKSO").
STRUCTURE
{
"id": ICAO code string, "name": display name, "icao": same ICAO, "iata": optional code,
"country": optional string, "nextSector": optional string like "<City> Control",
"transitionLevel": optional number (60 typical, 180 for US), "upperLimit": optional number (130-195),
"ref": { "lat": number, "lon": number }, // airport centre, decimal degrees, south/west negative
"runways": [ { "name": "13/31", "ends": [
{ "id": "13", "lat": n, "lon": n, "ils": 10, "approach": "RNP" },
{ "id": "31", "lat": n, "lon": n, "ils": 12, "approach": "ILS", "default": true } ] } ],
"waypoints": [ { "name": "TOLBA", "kind": "fix" | "vor" | "ndb", "lat": n, "lon": n }, ... ],
"procedures": [ { "name": "TOLBA1A", "kind": "STAR", "fixes": ["TOLBA", ...] },
{ "name": "REKSO1D", "kind": "SID", "fixes": ["REKSO"] }, ... ],
"airways": optional [ { "name": "A100", "fixes": [names...] } ],
"overflightRoutes": optional [ [names...], ... ],
"tmaRadius": optional number (NM)
}
RULES THE FILE MUST FOLLOW
1. Valid JSON only: no comments, no trailing commas.
2. Coordinates are decimal degrees; south and west are negative.
3. Runway end ids are two digits plus optional L/C/R (like "31L"); each runway lists both ends, roughly 2 NM apart, and the number matches the runway's direction (31 means about 310 degrees). "ils": 10-12 for an instrument runway, 0 for none. Put "default": true on one landing runway.
4. 8-20 waypoints spread around the field within about 60 NM: one "vor" at the field plus fixes in all directions.
5. At least 3 STARs and 3 SIDs. Every name inside "fixes" must EXACTLY match a waypoint name. Each STAR must END at the on-field vor (or a fix within 5 NM of "ref"). The LAST fix of a SID is where departures leave.
6. If you include airways/overflightRoutes, every fix in them must exist in waypoints; overflight routes cross the area entry to exit.
Answer with the JSON now.
Got the reply as text? Turn it into a file here
Paste the assistant's whole reply below (extra text around the JSON is fine — it is stripped automatically). This runs entirely in your browser and downloads a ready-to-upload .json file named after the airport.
Tips for a good airport
- Take real data from your country's AIP, or free sources like OurAirports — runway thresholds and navaid positions matter most.
- Keep entry fixes within roughly your
tmaRadiusso traffic spawns on-scope at the default range. - Give ILS runways
"approach": "ILS"so arrivals can be cleared for a precision approach. - Spread STAR entry points around the compass so arrivals converge from several directions.