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

FieldTypeMeaning
idstring, requiredUnique identifier, normally the ICAO code (e.g. OMDB). Must not clash with a built-in airport.
namestring, requiredDisplay name shown in the airport list and on the radar.
icaostring, requiredICAO location indicator, drawn next to the field on the scope.
iatastring, optionalIATA code (e.g. DXB).
countrystring, optionalCountry name. If omitted it is derived from the ICAO prefix.
transitionLevelnumber, optionalTransition 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.
upperLimitnumber, optionalUpper limit of your airspace (FL). Overflights cross below it, and departures’ planned “ATC FL” is at or just under it.
nextSectorstring, optionalThe facility departures and overflights are handed to (e.g. "Dubai Control").
refobject, required{ "lat", "lon" } in decimal degrees — the airport reference point, the centre of the radar. South and west are negative.
runwaysarray, requiredAt least one runway — see below.
waypointsarray, requiredAt least one named point — see below.
proceduresarray, requiredThe SIDs and STARs — at least one of each.
airwaysarray, optionalAirway lines drawn on the map.
overflightRoutesarray, optionalFix-name lists that overflying traffic follows across your airspace.
tmaRadiusnumber, optionalRadius (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:

FieldTypeMeaning
idstring, requiredThe 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 / lonnumber, requiredThe threshold position of this end, decimal degrees.
ilsnumber, requiredLength (NM) of the final-approach course drawn on the scope — typically 10–12. Use 0 for none.
approachstring, optionalApproach type label: "ILS", "RNP", "VOR"… ILS arrivals call “established localizer”; others call “established final”.
defaultboolean, optionalMarks the runway preselected in the New Session dialog.

Waypoints

FieldTypeMeaning
namestring, requiredThe 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.
kindstring, required"fix" (triangle), "vor" (hexagon) or "ndb" (circle).
lat / lonnumber, requiredPosition 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.

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:

What the upload check requires

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 endsEach 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 / SIDAdd 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 airportPick 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

⬇ Download the sample airport ▶ Upload it and fly