Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

input_output.md

maoxiaoyue edited this page Apr 3, 2026 · 1 revision

Input/Output 機制詳解

HypGo Framework — Schema Input/Output 完整技術指南 版本:0.8.1-alpha | 2026-04


總覽

在 HypGo 中,InputOutput 是 Schema-first 路由的核心概念。它們不只是文檔,而是可執行的合約 — 框架用它們來驗證請求、測試回應、生成 manifest。

Input(請求合約) Output(回應合約)
 │ │
 ├→ Contract Testing 驗證請求 body ├→ Contract Testing 驗證回應 body
 ├→ Manifest 記錄 input_type ├→ Manifest 記錄 output_type
 ├→ AI 知道要傳什麼 ├→ AI 知道會收到什麼
 └→ 自動生成測試資料 └→ 自動驗證必填欄位

第一部分:定義 Input/Output

基本規則

Input 和 Output 都是普通的 Go struct,透過 json tag 定義 JSON 欄位名:

// Input — 客戶端發送的請求 body
type CreateUserReq struct {
 Name string `json:"name"` // 必填(非 pointer、無 omitempty)
 Email string `json:"email"` // 必填
 Bio string `json:"bio,omitempty"` // 選填(有 omitempty)
 Age *int `json:"age"` // 選填(pointer)
}
// Output — 伺服器回傳的回應 body
type UserResp struct {
 ID int64 `json:"id"`
 Name string `json:"name"`
 Email string `json:"email"`
 Bio string `json:"bio,omitempty"`
 CreatedAt string `json:"created_at"`
}

必填 vs 選填的判斷規則

框架透過 reflect 自動判斷每個欄位是否為必填:

條件 判斷結果 範例
非 pointer + 無 omitempty 必填 Name string \json:"name"``
omitempty 選填 Bio string \json:"bio,omitempty"``
pointer 型別 選填 Age *int \json:"age"``
json:"-" 忽略(不出現在 JSON 中) Internal string \json:"-"``

內部實作(pkg/schema/reflect.go):

required := !omitempty && f.Type.Kind() != reflect.Ptr

這一行決定了 Contract Testing 是否會檢查該欄位的存在。

Input ≠ Output ≠ DB Model

三者有不同的職責,不應混用:

// ❌ 錯誤:用 DB model 當 Input
r.Schema(schema.Route{
 Input: models.User{}, // 包含 ID、CreatedAt — 客戶端不該提供
})
// ✅ 正確:分開定義
type User struct { // DB model — 對應資料庫 table
 ID int64
 Name string
 CreatedAt time.Time
}
type CreateUserReq struct { // Input — 只有客戶端可提供的欄位
 Name string `json:"name"`
 Email string `json:"email"`
}
type UserResp struct { // Output — 只有要回傳的欄位
 ID int64 `json:"id"`
 Name string `json:"name"`
 Email string `json:"email"`
}
struct 用途 包含 ID? 包含密碼? 在哪裡用
User DB model Bun ORM
CreateUserReq Input Schema Input
UserResp Output Schema Output

第二部分:在 Schema 中使用 Input/Output

註冊時傳入 struct 實例

r.Schema(schema.Route{
 Method: "POST",
 Path: "/api/users",
 Summary: "Create user",
 Input: CreateUserReq{}, // ← 傳入零值實例,框架只用型別資訊
 Output: UserResp{}, // ← 傳入零值實例
}).Handle(createUserHandler)

重要:傳的是 CreateUserReq{}(零值實例),不是指標 &CreateUserReq{}。框架只讀取型別資訊(reflect.TypeOf),不使用實例的值。

框架在註冊時做了什麼

當你呼叫 router.Schema(route).Handle(handler) 時,框架內部執行:

1. NewSchemaRoute(route, registrar)
 │
 ├─ route.Input != nil ?
 │ └─ route.InputName = TypeName(route.Input)
 │ → reflect.TypeOf(CreateUserReq{}).Name() → "CreateUserReq"
 │
 ├─ route.Output != nil ?
 │ └─ route.OutputName = TypeName(route.Output)
 │ → "UserResp"
 │
 └─ Responses 中的 Type != nil ?
 └─ resp.TypeName = TypeName(resp.Type)
2. registrar.RegisterSchema(route, handlers...)
 │
 ├─ router.addRoute(method, path, handlers) ← 正常路由註冊
 │
 └─ schema.Global().Register(route) ← 存入全域 Schema Registry
 → key = "POST /api/users"
 → value = Route{Input: CreateUserReq{}, Output: UserResp{}, ...}

Schema Registry 儲存了什麼

// 全域 Registry 結構
type Registry struct {
 schemas []Route // 有序列表
 byKey map[string]*Route // "METHOD /path" → Route
}
// 每個 Route 中與 Input/Output 相關的欄位:
type Route struct {
 Input interface{} // Go struct 零值(CreateUserReq{})
 Output interface{} // Go struct 零值(UserResp{})
 InputName string // "CreateUserReq"(自動填入)
 OutputName string // "UserResp"(自動填入)
}

InputOutput 欄位帶有 json:"-" tag,所以它們不會出現在 JSON/YAML 序列化中。Manifest 使用 InputName / OutputName(字串)。


第三部分:Input/Output 在各層的作用

3.1 Manifest(AI 理解層)

hyp context 或 AutoSync 產出的 manifest 中,Input/Output 以型別名稱呈現:

routes:
 - method: POST
 path: /api/users
 summary: "Create user"
 input_type: CreateUserReq # ← InputName
 output_type: UserResp # ← OutputName
 handler_names: [controllers.CreateUser]

AI 看到 input_type: CreateUserReq 就知道:

  1. 這個路由接受 CreateUserReq struct 作為 JSON body
  2. 可以去 app/models/ 找到 CreateUserReq 的欄位定義
  3. 生成的 handler 要用 c.ShouldBindJSON(&req) 解析

3.2 Contract Testing(驗證層)

自動測試(TestAll)

contract.TestAll(t, router) 對每個 schema 路由自動執行:

對每個路由:
 1. 有 Input?
 └─ generateMinimalJSON(route.Input)
 → 為每個欄位生成合理值:
 string → "test"
 int → 0
 bool → false
 slice → []
 → 結果:{"name":"test","email":"test","bio":"test"}
 2. 發送 HTTP 請求
 → method = route.Method
 → path = resolvePath(route.Path) // :id → "1"
 → body = 生成的 JSON
 3. 有 Output?
 └─ validateResponse(responseBody, route.Output)
 │
 ├─ json.Unmarshal(body, &OutputType{})
 │ → 確認 JSON 格式正確、欄位型別匹配
 │
 └─ validateRequiredFields(body, OutputType)
 → 遍歷 Output struct 的所有欄位
 → 非 pointer + 非 omitempty → 必填
 → 必填欄位不在 JSON 中 → 回報錯誤
 4. 驗證狀態碼
 → Responses 有宣告?用最小的 2xx 狀態碼
 → 沒有宣告?POST→201, DELETE→204, 其他→200

手動測試(Test)

contract.Test(t, router, contract.TestCase{
 Route: "POST /api/users",
 Input: `{"name":"alice","email":"alice@example.com"}`,
 ExpectStatus: 201,
 ExpectSchema: true, // ← 啟用 Output 驗證
})

ExpectSchema: true 時,框架會:

  1. 從 Schema Registry 查找 "POST /api/users" 的 Output 型別
  2. 把回應 body json.UnmarshalUserResp{}
  3. 檢查 UserResp 中所有必填欄位都存在於回應中

3.3 Scaffold(生成層)

hyp generate model user 自動生成配對的 struct:

// 生成的 Input struct
type CreateUserReq struct {
 Name string `json:"name"` // 必填
 Description string `json:"description,omitempty"` // 選填
}
type UpdateUserReq struct {
 Name string `json:"name,omitempty"` // 全部選填(部分更新)
 Description string `json:"description,omitempty"`
 Active *bool `json:"active,omitempty"` // pointer = 選填
}

hyp generate controller user 生成的 handler 自動使用:

func (ctrl *UserController) Create(c *hypcontext.Context) {
 var req models.CreateUserReq // ← 引用 Input struct
 if err := c.ShouldBindJSON(&req); err != nil {
 errors.AbortWithAppError(c, ErrUserInvalid.With("reason", err.Error()))
 return
 }
 // ... 業務邏輯 ...
 c.JSON(201, models.UserResp{ // ← 引用 Output struct
 ID: user.ID,
 Name: req.Name,
 })
}

3.4 AI Rules(跨工具層)

hyp ai-rules 生成的 AGENTS.md 包含路由表(如果 manifest 存在):

## Current Routes
| Method | Path | Summary |
|--------|------|---------|
| POST | /api/users | Create user |
| GET | /api/users/:id | Get user |

AI 工具看到這個表格,就知道去 manifest 查 input_typeoutput_type


第四部分:型別反射機制

TypeName — 取得型別名稱

schema.TypeName(CreateUserReq{}) → "CreateUserReq"
schema.TypeName(&CreateUserReq{}) → "CreateUserReq" // 自動解指標
schema.TypeName((*UserResp)(nil)) → "UserResp" // nil 指標也行
schema.TypeName(nil) → ""
schema.TypeName(42) → "int"

FieldsOf — 取得欄位清單

fields := schema.FieldsOf(CreateUserReq{})
// [
// {Name: "name", Type: "string", Required: true},
// {Name: "email", Type: "string", Required: true},
// {Name: "bio", Type: "string", Required: false}, // omitempty
// {Name: "age", Type: "integer", Required: false}, // pointer
// ]

ValidateJSON — 驗證 JSON 合規性

// 通過
err := schema.ValidateJSON(
 []byte(`{"name":"alice","email":"a@b.com"}`),
 CreateUserReq{},
)
// err == nil
// 失敗:缺少必填欄位
err := schema.ValidateJSON(
 []byte(`{"bio":"hello"}`),
 CreateUserReq{},
)
// err == "missing required field: name"
// 失敗:JSON 格式錯誤
err := schema.ValidateJSON(
 []byte(`{invalid json}`),
 CreateUserReq{},
)
// err == "JSON does not match schema CreateUserReq: ..."

GenerateZeroJSON — 生成零值 JSON

data := schema.GenerateZeroJSON(CreateUserReq{})
// {"name":"","email":"","bio":"","age":null}
data := schema.GenerateZeroJSON(nil)
// {}

Go 型別 → Schema 型別對照

Go 型別 Schema 型別
string "string"
int, int64, uint "integer"
float32, float64 "number"
bool "boolean"
[]T "array"
map[K]V, struct "object"

第五部分:完整生命週期

一個 Input/Output struct 從定義到驗證的完整路徑:

👤 人定義 struct
 │
 ▼
┌──────────────────────────────────────────────────────┐
│ type CreateUserReq struct { │
│ Name string `json:"name"` │
│ Email string `json:"email"` │
│ } │
└──────────────────────────────────────────────────────┘
 │
 ▼
👤 人在 Schema 路由中引用
 │
 ▼
┌──────────────────────────────────────────────────────┐
│ r.Schema(schema.Route{ │
│ Input: CreateUserReq{}, ← 型別資訊進入框架 │
│ Output: UserResp{}, │
│ }).Handle(handler) │
└──────────────────────────────────────────────────────┘
 │
 ├──→ 🔧 Schema Registry 儲存型別
 │ key: "POST /api/users"
 │ Input: CreateUserReq{}(reflect.Type)
 │ InputName: "CreateUserReq"(string)
 │
 ├──→ 🔧 Manifest 輸出型別名稱
 │ input_type: CreateUserReq
 │ output_type: UserResp
 │
 ├──→ 🤖 AI 讀 manifest → 知道 Input/Output
 │ → 生成 handler 時使用正確的 struct
 │
 └──→ 🔧 Contract Testing 驗證
 TestAll()
 │
 ├─ generateMinimalJSON(CreateUserReq{})
 │ → {"name":"test","email":"test"}
 │
 ├─ 發送 POST /api/users,body = 上述 JSON
 │
 └─ validateResponse(body, UserResp{})
 → json.Unmarshal 到 UserResp
 → 檢查 id、name、email 都存在

第六部分:常見模式

列表回應(帶分頁)

type UserListResp struct {
 Data []UserResp `json:"data"`
 Total int `json:"total"`
}
r.Schema(schema.Route{
 Method: "GET",
 Path: "/api/users",
 Output: UserListResp{}, // ← 列表用 ListResp
}).Handle(ctrl.List)

部分更新(PATCH/PUT)

type UpdateUserReq struct {
 Name string `json:"name,omitempty"` // 全部 omitempty
 Email string `json:"email,omitempty"`
 Active *bool `json:"active,omitempty"` // 用 pointer 區分「沒傳」vs「傳 false」
}

無 body 的路由

// GET — 無 Input
r.Schema(schema.Route{
 Method: "GET",
 Path: "/api/users/:id",
 Output: UserResp{}, // 只有 Output
}).Handle(ctrl.Get)
// DELETE — 無 Input、無 Output
r.Schema(schema.Route{
 Method: "DELETE",
 Path: "/api/users/:id",
 // Input: nil, Output: nil
}).Handle(ctrl.Delete)

錯誤回應

錯誤不用定義在 Output 中。HypGo 使用 Error Catalog 統一處理:

// 定義錯誤碼
var ErrUserNotFound = errors.Define("E_user_001", 404, "User not found", "user")
// Handler 中使用
func (ctrl *UserController) Get(c *hypcontext.Context) {
 user := findUser(id)
 if user == nil {
 errors.AbortWithAppError(c, ErrUserNotFound.With("id", id))
 return // ← 回傳 {"code":"E_user_001","message":"User not found","details":{"id":"1"}}
 }
 c.JSON(200, UserResp{...}) // ← 回傳 Output struct
}

Contract Testing 只驗證成功路徑的 Output。錯誤路徑由 Error Catalog 保證格式一致。


第七部分:除錯技巧

查看某個路由的 Input/Output

route, ok := schema.Global().Get("POST", "/api/users")
if ok {
 fmt.Printf("Input: %s\n", route.InputName) // "CreateUserReq"
 fmt.Printf("Output: %s\n", route.OutputName) // "UserResp"
 // 查看 Input 欄位
 fields := schema.FieldsOf(route.Input)
 for _, f := range fields {
 fmt.Printf(" %s (%s) required=%v\n", f.Name, f.Type, f.Required)
 }
}

手動驗證 JSON

err := schema.ValidateJSON(
 []byte(`{"name":"alice"}`), // 缺少 email
 CreateUserReq{},
)
fmt.Println(err) // "missing required field: email"

查看 Contract Testing 為某路由生成的測試資料

route := schema.Route{
 Method: "POST",
 Path: "/api/users",
 Input: CreateUserReq{},
}
// 框架內部用的是 generateMinimalJSON(unexported),但你可以用 GenerateZeroJSON
data := schema.GenerateZeroJSON(CreateUserReq{})
fmt.Println(string(data))
// {"name":"","email":"","bio":"","age":null}

HypGo · Input/Output 機制詳解 · 2026-04

HypGo

繁體中文 | English


中文文件

設計文件

套件

AI 協作工具鏈

CLI 命令


English Docs

Design Docs

Packages

AI Collaboration Toolchain

CLI Commands

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /