{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/chinafootnotes-com/json-schema/chinafootnotes-com-city-detail-schema.json",
  "title": "CityDetail",
  "x-generated": "2026-11-03",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/chinafootnotes-com-openapi.yml#/components/schemas/CityDetail",
  "type": "object",
  "properties": {
    "schemaVersion": {
      "type": "string"
    },
    "version": {
      "type": "string"
    },
    "generatedAt": {
      "type": "string",
      "format": "date-time"
    },
    "notice": {
      "type": "string"
    },
    "city": {
      "$ref": "#/$defs/City"
    },
    "places": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "nameZh": {
            "type": "string"
          },
          "shortName": {
            "type": "string"
          },
          "area": {
            "type": [
              "string",
              "null"
            ]
          },
          "group": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": "string"
          },
          "api": {
            "type": "string"
          }
        }
      }
    }
  },
  "required": [
    "city",
    "places"
  ],
  "$defs": {
    "City": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "zh": {
          "type": "string"
        },
        "live": {
          "type": "boolean"
        },
        "preview": {
          "type": "boolean",
          "description": "Only in local preview builds of unreviewed data"
        },
        "placeCount": {
          "type": "integer"
        },
        "accent": {
          "type": "string",
          "description": "#rrggbb"
        },
        "accentSoft": {
          "type": "string",
          "description": "#rrggbb"
        },
        "tagline": {
          "type": [
            "string",
            "null"
          ],
          "description": "null until written"
        },
        "stats": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "value": {
                "type": [
                  "number",
                  "string",
                  "null"
                ]
              },
              "unit": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "source": {},
              "asOf": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "description": "null until collected"
        },
        "weather": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "month": {
                "type": "integer",
                "minimum": 1,
                "maximum": 12
              },
              "highC": {
                "type": "number"
              },
              "lowC": {
                "type": "number"
              },
              "icon": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "sun",
                  "cloud",
                  "rain",
                  "snow",
                  "haze",
                  null
                ]
              },
              "rainDays": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "source": {}
            }
          },
          "description": "12 months, or null"
        },
        "areas": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "zh": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "line": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "numbers": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "focus": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "groups": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "town": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            },
            "trips": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "number": {
                    "type": "integer"
                  },
                  "distanceKm": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "bookFirst": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Place ids that need booking ahead, from the place booking data"
        },
        "arrivals": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "zh": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "airport",
                  "station"
                ]
              },
              "how": {
                "type": "string",
                "description": "One-line summary"
              },
              "routes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "mode": {
                      "type": "string",
                      "enum": [
                        "metro",
                        "maglev",
                        "rail",
                        "bus",
                        "taxi"
                      ]
                    },
                    "text": {
                      "type": "string"
                    },
                    "fare": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Yuan; min null = up to max",
                      "properties": {
                        "min": {
                          "type": [
                            "number",
                            "null"
                          ]
                        },
                        "max": {
                          "type": "number"
                        },
                        "approx": {
                          "type": "boolean"
                        }
                      }
                    },
                    "durationMin": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "lastService": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "HH:MM"
                    },
                    "serviceHours": {
                      "type": "string",
                      "description": "HH:MM–HH:MM"
                    }
                  }
                }
              },
              "caveats": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string"
                    },
                    "sourceIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "tips": {
                "type": "array",
                "description": "Traveller reports, each with its evidence",
                "items": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "body": {
                      "type": "string"
                    },
                    "caveat": {
                      "type": "string"
                    },
                    "evidence": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              },
              "gaps": {
                "type": "array",
                "description": "Not found yet, with the search record",
                "items": {
                  "type": "object",
                  "properties": {
                    "reason": {
                      "type": "string"
                    },
                    "searched": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "nextStep": {
                      "type": "string"
                    }
                  }
                }
              },
              "checkedAt": {
                "type": "string",
                "format": "date"
              },
              "sourceIds": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Ids in City.sources"
              }
            }
          }
        },
        "nextStops": {
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string"
              },
              "trainTime": {},
              "source": {}
            }
          },
          "description": "Other cities reachable by train, listed only when that city is published in this build"
        },
        "photo": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "src": {
              "type": "string"
            },
            "credit": {
              "type": "string"
            },
            "href": {
              "type": "string"
            },
            "rights": {
              "type": "string"
            }
          }
        },
        "skyline": {
          "type": [
            "string",
            "null"
          ]
        },
        "scene": {
          "type": [
            "object",
            "null"
          ]
        },
        "metroColors": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 2,
            "maxItems": 2
          },
          "description": "Line → [background, ink]"
        },
        "ratings": {
          "type": [
            "object",
            "null"
          ],
          "description": "Editorial ratings comparing the cities on this site; subjective, not an official ranking. plan = how early to book (shown as calendars; more = book earlier), not better or worse.",
          "properties": {
            "history": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "stars"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            },
            "food": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "stars"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            },
            "cityscape": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "stars"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            },
            "nature": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "stars"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            },
            "shopping": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "stars"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            },
            "plan": {
              "type": "object",
              "properties": {
                "stars": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 5,
                  "multipleOf": 0.5
                },
                "display": {
                  "type": "string",
                  "enum": [
                    "calendar"
                  ]
                },
                "overview": {
                  "type": "string",
                  "maxLength": 220,
                  "description": "Shown when the reader opens the rating"
                },
                "reason": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Why this rating; may repeat the overview"
                }
              },
              "required": [
                "stars",
                "display",
                "overview",
                "reason"
              ]
            }
          },
          "required": [
            "history",
            "food",
            "cityscape",
            "nature",
            "shopping",
            "plan"
          ]
        },
        "images": {
          "type": [
            "object",
            "null"
          ],
          "description": "City illustrations: hero (city page header), card (city card, cropped from the hero at objectPosition) and bg (home background)",
          "properties": {
            "hero": {
              "type": "object",
              "properties": {
                "sizes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "src": {
                        "type": "string"
                      },
                      "width": {
                        "type": "integer"
                      },
                      "height": {
                        "type": "integer"
                      }
                    },
                    "required": [
                      "src",
                      "width",
                      "height"
                    ]
                  },
                  "description": "WebP sizes, small to large (use as srcset)"
                },
                "objectPosition": {
                  "type": "string"
                }
              }
            },
            "card": {
              "type": "object",
              "properties": {
                "from": {
                  "type": "string",
                  "enum": [
                    "hero"
                  ]
                },
                "objectPosition": {
                  "type": "string"
                }
              }
            },
            "bg": {
              "type": "object",
              "properties": {
                "sizes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "src": {
                        "type": "string"
                      },
                      "width": {
                        "type": "integer"
                      },
                      "height": {
                        "type": "integer"
                      }
                    },
                    "required": [
                      "src",
                      "width",
                      "height"
                    ]
                  },
                  "description": "WebP sizes, small to large (use as srcset)"
                },
                "objectPosition": {
                  "type": "string"
                }
              }
            }
          }
        },
        "transit": {
          "type": [
            "object",
            "null"
          ],
          "description": "The city metro: how to pay and ride",
          "properties": {
            "name": {
              "type": "string"
            },
            "zh": {
              "type": "string"
            },
            "summary": {
              "type": "string"
            },
            "steps": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "fares": {
              "type": "object",
              "description": "Yuan amounts and a note"
            },
            "caveats": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "sourceIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "tips": {
              "type": "array",
              "description": "Traveller reports, each with its evidence",
              "items": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "caveat": {
                    "type": "string"
                  },
                  "evidence": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            },
            "checkedAt": {
              "type": "string",
              "format": "date"
            },
            "sourceIds": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "sources": {
          "type": "array",
          "description": "Sources cited by arrivals, transit and notes (sourceIds)",
          "items": {
            "$ref": "#/$defs/Source"
          }
        }
      },
      "required": [
        "id",
        "name",
        "zh",
        "live",
        "placeCount",
        "accent",
        "accentSoft"
      ]
    },
    "Source": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "url": {
          "type": [
            "string",
            "null"
          ],
          "format": "uri",
          "description": "null only for our own checks, which have no web page: kind owner_tested (we tried it ourselves; first-hand, not official) or owner_official (the venue's own answer: method phone, onsite_sign or onsite_notice; official). accessedAt is the day we checked."
        },
        "title": {
          "type": "string"
        },
        "publisher": {
          "type": "string"
        },
        "kind": {
          "type": "string",
          "enum": [
            "official",
            "government"
          ]
        },
        "method": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "phone",
            "onsite_sign",
            "onsite_notice",
            null
          ],
          "description": "Only for kind owner_official: how we got the venue’s answer (by phone, a standing sign on site, or a temporary notice on site)."
        },
        "contact": {
          "type": [
            "string",
            "null"
          ],
          "description": "Only for method phone: who we called (venue and number)."
        },
        "accessedAt": {
          "type": "string",
          "format": "date"
        },
        "publishedAt": {
          "type": [
            "string",
            "null"
          ],
          "format": "date"
        }
      },
      "required": [
        "id",
        "url",
        "title",
        "publisher",
        "kind",
        "accessedAt"
      ]
    }
  }
}