跳到正文
Hacker News· ahl·· 6 天前AI 评分22

quicktype:Copilot 之前最高产的代码生成器,累计生成 100 亿行代码

Ten Billion Lines of Code-The most prolific code generator before Copilot (2025)

AI 导读

quicktype 自 2017 年 7 月发布以来累计生成 100 亿行代码,且没有一行由 LLM 写出,发布一周年时已突破 10 亿行。它从 JSON 生成强类型模型与序列化代码,支持 TypeScript、Python、Go、Rust、Swift 等 30 多种语言,采用类型图与语言渲染器分离的架构,便于社区扩展语言。

正文 · AI 翻译

一百亿行代码,没有一行是 LLM 写的。这是对 quicktype 自 2017 年以来生成代码量的保守估计,当时地球上最多产的程序员可以说是一个将 JSON 转换为类型的小型开源工具。我构建第一个版本是因为我厌倦了手写 Swift 样板代码。

我和 Mark 在 2017 年 7 月发布了 quicktype,用于从 JSON 生成强类型模型和序列化器。到它一岁生日时,我们已经突破十亿行。

它支持 30 多种语言——TypeScript、Python、Go、Rust、Swift、Kotlin、Java、C#、C++、Haskell、Dart、Elm 等——每种语言都能生成地道的、人类质量的代码。我们构建的架构在类型图和特定语言渲染器之间做了清晰的分离,这使得社区能够添加其中许多语言。

起源故事

2016 年,我在 Swift 4 和 Codable 之前构建一个 Swift 应用。当时没有好的 JSON 序列化方式——你必须为每个模型类型编写繁琐的样板代码:

struct User {
    let name: String
    let email: String
    let age: Int

    init?(json: [String: Any]) {
        guard let name = json["name"] as? String,
              let email = json["email"] as? String,
              let age = json["age"] as? Int else {
            return nil
        }
        self.name = name
        self.email = email
        self.age = age
    }

    func toJSON() -> [String: Any] {
        return ["name": name, "email": email, "age": age]
    }
}

每次我更改数据模型时,都必须手动更新序列化方法。这是计算机该做的工作。

我拼凑了一个原型——粘贴 JSON,得到 Swift 结构体——然后展示给 Mark。他在类型系统和编译器方面有深厚的专业知识,立刻看出这不仅仅是字符串处理。如何从样本数据中推断最佳类型?如何处理异构数组?如何知道一个对象实际上是字典?

前提很简单:粘贴 JSON,得到类型安全的代码。但实现需要解决一些出奇深刻的计算机科学问题。

架构:读取、简化、渲染

quicktype 像编译器一样分三个阶段处理 JSON(深入解析见 quicktype Under the Hood):

读取:解析 JSON 并推断初始类型图。每个唯一结构成为一个节点——对象数组会创建一个包含 Class 类型的 Array 类型。

简化:优化类型图。当同一位置可以包含不同类型时创建联合类型([1, "1"] → Array<Int | String>)。合并等价类型。使用马尔可夫链检测映射与对象(详见下文)。

渲染:将类型图转换为源代码。每种语言都有自己的渲染器,处理命名约定、序列化、类型语法和导入。

这种架构使添加语言变得容易——只需实现一个新的渲染器。这就是我们从发布时的 2 种语言发展到 30 多种语言的方式。

小大细节

我们称之为“小大细节”——那些让 quicktype 的输出看起来像人类编写的微妙之处。

属性名

JSON 属性名形形色色:

{
  "user_name": "alice",
  "firstName": "Alice",
  "LAST-NAME": "Smith",
  "EmailAddress": "alice@example.com"
}

quicktype 检测命名风格,将名称拆分为单词,按目标语言的约定重新组合,并在需要时生成映射代码。对于 Swift:

struct User: Codable {
    let userName: String
    let firstName: String
    let lastName: String
    let emailAddress: String

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
        case firstName
        case lastName = "LAST-NAME"
        case emailAddress = "EmailAddress"
    }
}

上下文类名

考虑这个 JSON:

{
  "user": {
    "address": { "street": "123 Main St", "city": "Springfield" }
  },
  "company": {
    "address": {
      "street": "456 Business Ave",
      "city": "Commerce City",
      "suite": "100"
    }
  }
}

两者都有一个结构不同的 address。朴素的方法会将两者都命名为 Address 并导致冲突。quicktype 使用上下文命名:

interface User {
  address: UserAddress;
}

interface UserAddress {
  street: string;
  city: string;
}

interface Company {
  address: CompanyAddress;
}

interface CompanyAddress {
  street: string;
  city: string;
  suite: string;
}

仅在必要时添加前缀。

用马尔可夫链检测映射

我最喜欢的创新(我写了一篇关于它的博客文章)。考虑这个比特币区块链数据:

{
  "000000000000000000c846dee2e13c6408f5": {
    "hash": "000000000000000000c846dee2e13c6408f5",
    "height": 503162,
    "time": 1515187634
  },
  "00000000000000000024fb37364cbf81fd95": {
    "hash": "00000000000000000024fb37364cbf81fd95",
    "height": 503163,
    "time": 1515188162
  }
}

朴素的转换会创建一个带有 the000000000000000000C846Dee2E13C6408F5 等属性的类——显然是错误的。人类会立即认出这是一个 Map<String, Block>。

为什么?这些属性名看起来不像属性名——它们看起来像数据。我们用一个在典型属性名上训练的马尔可夫链来量化这一直觉。该链对字符序列概率建模:qu → i 是高概率,qu → x 则接近于零。

quicktype 会根据这个模型对每个属性名打分。像 firstName 这样的名字得分很高;哈希值则接近于零。低于某个阈值时,它会生成一个 map:

typealias Blocks = [String: Block]

struct Block: Codable {
    let hash: String
    let height: Int
    let time: Int
}

多个样本以获得更好的类型

单个 JSON 样本可能无法代表所有可能的值(更多细节)。提供两个样本:

// Sample 1: User is online
{ "name": "Alice", "status": "online", "lastSeen": null }

// Sample 2: User is offline
{ "name": "Bob", "status": "offline", "lastSeen": "2018-03-08T10:30:00Z" }

而 quicktype 通过合并所有样本的类型图,正确地推断出 lastSeen: string | null。

TypeScript 作为模式语言

你可以编写 TypeScript 类型定义并将其作为输入:

interface Person {
  name: string;
  nickname?: string; // an optional property
  luckyNumber: number;
}

TypeScript 比 JSON Schema 更易读,使其成为一种出色的“模式语言”:

quicktype pokedex.json -o pokedex.ts --just-types  # Infer types from JSON
quicktype pokedex.ts -o src/ios/models.swift       # Generate Swift from TypeScript

跨语言类型转换

quicktype 还可以读取一种语言的源文件,并在另一种语言中生成等价的类型:

quicktype --src-lang csharp Models.cs -o Models.swift

我们学到了什么

大多数开发者并不了解 JSON。我们期望的是合法的 JSON。我们得到的是尾随逗号、单引号、未加引号的键、注释、JavaScript 对象。我们构建了越来越复杂的错误恢复机制,来处理人们粘贴进来的垃圾内容。

细节很重要。属性命名、上下文类名、map 检测——这些让 quicktype 显得神奇。看起来像人写的生成代码会促进采用。

在开发者所在之处与他们相遇。Web 应用是个很棒的演示,但采用率是在 CLI 和 IDE 集成之后才爆发的。

开源行之有效。Rust、Kotlin 和 Python 渲染器来自贡献者。我们自己不可能构建 30 多种语言。

为什么 quicktype 仍然重要

在 Copilot 和 ChatGPT 的时代,quicktype 做了 LLM 难以做到的事:你给它的数据越多,它就越能理解你的模式。给 quicktype 一百个 JSON 样本,它会生成更准确的类型。给 LLM 一百个样本,它会触及上下文限制或凭空捏造属性。

quicktype 是确定性的。相同的输入,相同的输出。它不猜测——它通过有原则的算法推断类型。这种可预测性对生产代码很重要。

在 quicktype.io 上试一试,或在 GitHub 上贡献。

来源:Hacker News · dvdsgl.co