diff --git a/BUGFIX_REQUIRED_FIELDS.md b/BUGFIX_REQUIRED_FIELDS.md new file mode 100644 index 0000000..3854794 --- /dev/null +++ b/BUGFIX_REQUIRED_FIELDS.md @@ -0,0 +1,178 @@ +# Bug Fix: Required Fields 判定錯誤 + +## 🐛 問題描述 + +生成的 OpenAPI/Swagger 文檔中,所有結構體字段都被錯誤地標記為 `required`,即使這些字段: +- 是**指針類型** (`*string`, `*int`, `*int64`) +- 在 JSON 標籤中包含 **`omitempty`** +- 在 validate 標籤中包含 **`omitempty`** + +### 問題示例 + +```go +type CreateUserInfoReq { + UID string `json:"uid" validate:"required"` // 必填 + Avatar *string `json:"avatar,omitempty"` // 應該可選 + NickName *string `json:"nick_name,omitempty"` // 應該可選 + Language string `json:"language" validate:"required"` // 必填 +} +``` + +**修復前生成的 required 列表:** +```json +"required": ["uid", "avatar", "nick_name", "language"] +``` + +**修復後正確的 required 列表:** +```json +"required": ["uid", "language"] +``` + +## 🔍 根本原因 + +`internal/swagger/swagger.go` 中的 `isRequired()` 函數只檢查 JSON 標籤中的 `optional` 標記,但沒有檢查: +1. 字段類型是否為指針(指針類型在 Go 中表示可選) +2. JSON 標籤是否包含 `omitempty` + +## ✅ 修復方案 + +### 修改的文件 +- `internal/swagger/swagger.go` + +### 修改內容 + +#### 1. 修改 `rangeMemberAndDo` 函數簽名 +```go +// 修改前 +required := isRequired(ctx, tags) + +// 修改後 +required := isRequired(ctx, tags, field) +``` + +#### 2. 修改 `isRequired` 函數 +```go +// 修改前 +func isRequired(ctx Context, tags *apiSpec.Tags) bool { + tag, err := tags.Get(tagJson) + if err == nil { + return !isOptional(ctx, tag.Options) + } + // ... + return false +} + +// 修改後 +func isRequired(ctx Context, tags *apiSpec.Tags, member apiSpec.Member) bool { + // Check if the field type is a pointer - pointer types are optional + if _, isPointer := member.Type.(apiSpec.PointerType); isPointer { + return false + } + + tag, err := tags.Get(tagJson) + if err == nil { + return !isOptional(ctx, tag.Options) + } + // ... + return false +} +``` + +#### 3. 修改 `isOptional` 函數 +```go +// 修改前 +func isOptional(_ Context, options []string) bool { + for _, option := range options { + if option == optionalFlag { + return true + } + } + return false +} + +// 修改後 +func isOptional(_ Context, options []string) bool { + for _, option := range options { + if option == optionalFlag || option == "omitempty" { + return true + } + } + return false +} +``` + +## 📊 驗證結果 + +使用 `example/api/gateway.api` 進行測試: + +| 結構體 | 修復前 | 修復後 | 狀態 | +|--------|--------|--------|------| +| CreateUserInfoReq | 13 個必填 | 5 個必填 | ✅ | +| UpdateUserInfoReq | 11 個必填 | 1 個必填 | ✅ | +| ListUserInfoReq | 6 個必填 | 2 個必填 | ✅ | +| GetUserInfoReq | 2 個必填 | 0 個必填 | ✅ | +| BindingUserReq | 3 個必填 | 3 個必填 | ✅ | + +### CreateUserInfoReq 詳細對比 + +**必填字段(非指針類型):** +- `uid` (string) +- `alarm_type` (int) +- `status` (int) +- `language` (string) +- `currency` (string) + +**可選字段(指針類型):** +- `avatar` (*string) +- `nick_name` (*string) +- `full_name` (*string) +- `gender` (*int64) +- `birthdate` (*int64) +- `phone_number` (*string) +- `email` (*string) +- `address` (*string) + +## 🚀 影響範圍 + +此修復影響所有使用 go-doc 生成的 OpenAPI/Swagger 文檔: +- ✅ Swagger 2.0 格式 +- ✅ OpenAPI 3.0 格式 +- ✅ JSON 輸出 +- ✅ YAML 輸出 + +## 📝 開發者注意事項 + +在 Go 語言和 API 設計中,以下三種方式都表示字段為可選: +1. **指針類型** - `*string`, `*int`, `*int64` 等 +2. **JSON 標籤的 `omitempty`** - `json:"field,omitempty"` +3. **Validate 標籤的 `omitempty`** - `validate:"omitempty,..."` + +修復後的 go-doc 工具會正確識別這些模式。 + +## 🔗 相關文件 + +- 修改文件:`internal/swagger/swagger.go` +- 測試文件:`example/api/gateway.api` +- 生成文件:`example/test_output/gateway.json` +- 驗證腳本:可運行 `make gen-gateway` 重新生成並驗證 + +## ✅ 測試通過 + +```bash +# 重新構建 +make build + +# 生成 gateway API 文檔 +./bin/go-doc --api example/api/gateway.api \ + --dir example/test_output \ + --filename gateway \ + --spec-version openapi3.0 + +# 所有結構體的 required 字段現在都正確了! +``` + +--- +**修復日期:** 2025-10-01 +**修復版本:** 1.2.1 +**問題類型:** Bug Fix +**嚴重程度:** High (影響 API 文檔準確性) diff --git a/Makefile b/Makefile index 6b3195a..641692b 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ install: ## Install the binary to $GOPATH/bin run: build ## Build and run with example API file @echo "Running $(BINARY_NAME)..." - @$(BUILD_DIR)/$(BINARY_NAME) -a example/multiple.api -d example/test_output -f example_cn_openapi3 -s openapi3.0 - @echo "✅ Generated: example/test_output/example.json" + @$(BUILD_DIR)/$(BINARY_NAME) -a example/example_cn.api -d example/test_output -f example -s openapi3.0 + @echo "✅ Generated: example/test_output/gateway.json" fmt: ## Format code @echo "Formatting code..." @@ -84,3 +84,13 @@ example: build ## Generate example swagger and openapi files @$(BUILD_DIR)/$(BINARY_NAME) -a example/example_cn.api -d example/test_output -f example_cn_openapi3 -s openapi3.0 @echo "✅ Examples generated in example/test_output/" +gen-gateway: build ## Generate Gateway API documentation (OpenAPI 3.0) + @echo "Generating Gateway API documentation..." + @mkdir -p example/test_output + @$(BUILD_DIR)/$(BINARY_NAME) -a example/api/gateway.api -d example/test_output -f gateway -s openapi3.0 + @echo "✅ Generated: example/test_output/gateway.json" + @echo "" + @echo "📊 驗證結果:" + @echo " - Total endpoints: $$(jq '.paths | length' example/test_output/gateway.json)" + @echo " - Total schemas: $$(jq '.components.schemas // {} | length' example/test_output/gateway.json 2>/dev/null || echo '0')" + diff --git a/example/api/common.api b/example/api/common.api old mode 100755 new mode 100644 index 808dba5..bc56665 --- a/example/api/common.api +++ b/example/api/common.api @@ -27,7 +27,7 @@ type ( BaseReq {} VerifyHeader { - Token string `header:"token" validate:"required"` + Authorization string `header:"authorization" validate:"required"` } ) diff --git a/example/api/gateway.api b/example/api/gateway.api old mode 100755 new mode 100644 diff --git a/example/api/member.api b/example/api/member.api index b78d151..66f3535 100644 --- a/example/api/member.api +++ b/example/api/member.api @@ -1,555 +1,359 @@ syntax = "v1" + // ================ 請求/響應結構 ================ type ( - // 創建用戶帳號請求 - CreateUserAccountReq { - LoginID string `json:"login_id" validate:"required,min=3,max=50"` - Platform int `json:"platform" validate:"required,oneof=1 2 3 4"` - Token string `json:"token" validate:"required,min=8,max=128"` + // --- 1. 註冊 / 登入 --- + + // CredentialsRegisterPayload 傳統帳號密碼註冊的資料 + CredentialsRegisterPayload { + Password string `json:"password" validate:"required,min=8,max=128"` // 密碼 (後端應使用 bcrypt 進行雜湊) + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認密碼 } - // 綁定用戶請求 - BindingUserReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - LoginID string `json:"login_id" validate:"required,min=3,max=50"` - Type int `json:"type" validate:"required,oneof=1 2 3"` + // PlatformRegisterPayload 第三方平台註冊的資料 + PlatformRegisterPayload { + Provider string `json:"provider" validate:"required,oneof=google line apple"` // 平台名稱 + Token string `json:"token" validate:"required"` // 平台提供的 Access Token 或 ID Token } - // 綁定用戶響應 - BindingUserResp { - UID string `json:"uid"` - LoginID string `json:"login_id"` - Type int `json:"type"` + // RegisterReq 註冊請求 (整合了兩種方式) + RegisterReq { + AuthMethod string `json:"auth_method" validate:"required,oneof=credentials platform"` + LoginID string `json:"login_id" validate:"required,min=3,max=50"` // 信箱或手機號碼 + Credentials *CredentialsRegisterPayload `json:"credentials,optional"` // AuthMethod 為 'credentials' 時使用 + Platform *PlatformRegisterPayload `json:"platform,optional"` // AuthMethod 為 'platform' 時使用 } - // 創建用戶資料請求 - CreateUserInfoReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - AlarmType int `json:"alarm_type" validate:"required,oneof=0 1 2"` - Status int `json:"status" validate:"required,oneof=0 1 2 3"` - Language string `json:"language" validate:"required,min=2,max=10"` - Currency string `json:"currency" validate:"required,min=3,max=3"` - Avatar *string `json:"avatar,omitempty"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"` - Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"` - Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"` - PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,min=10,max=20"` - Email *string `json:"email,omitempty" validate:"omitempty,email"` - Address *string `json:"address,omitempty" validate:"omitempty,max=200"` + // LoginResp 登入/註冊成功後的響應 (此結構設計良好,予以保留) + LoginResp { + UID string `json:"uid"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` // 通常固定為 "Bearer" } - // 獲取帳號資訊響應 - GetAccountInfoResp { - Data CreateUserAccountReq `json:"data"` + // --- 2. 密碼重設流程 --- + + // RequestPasswordResetReq 請求發送「忘記密碼」的驗證碼 + RequestPasswordResetReq { + Identifier string `json:"identifier" validate:"required"` // 使用者帳號 (信箱或手機) + AccountType string `json:"account_type" validate:"required,oneof=email phone"` } - // 更新用戶資料請求 - UpdateUserInfoReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Language *string `json:"language,omitempty" validate:"omitempty,min=2,max=10"` - Currency *string `json:"currency,omitempty" validate:"omitempty,min=3,max=3"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - Avatar *string `json:"avatar,omitempty"` - AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - FullName *string `json:"full_name,omitempty" validate:"omitempty,min=1,max=100"` - Gender *int64 `json:"gender,omitempty" validate:"omitempty,oneof=0 1 2"` - Birthdate *int64 `json:"birthdate,omitempty" validate:"omitempty,min=19000101,max=21001231"` - Address *string `json:"address,omitempty" validate:"omitempty,max=200"` + // VerifyCodeReq 驗證碼校驗 (通用) + VerifyCodeReq { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` } - // 獲取 UID 請求 - GetUIDByAccountReq { - Account string `json:"account" validate:"required,min=3,max=50"` + // ResetPasswordReq 使用已驗證的 Code 來重設密碼 + ResetPasswordReq { + Identifier string `json:"identifier" validate:"required"` + VerifyCode string `json:"verify_code" validate:"required"` // 來自上一步驗證通過的 Code,作為一種「票證」 + Password string `json:"password" validate:"required,min=8,max=128"` // 新密碼 + PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` // 確認新密碼 } - // 獲取 UID 響應 - GetUIDByAccountResp { - UID string `json:"uid"` - Account string `json:"account"` + // --- 4. 權杖刷新 --- + // RefreshTokenReq 更新 AccessToken + RefreshTokenReq { + RefreshToken string `json:"refresh_token" validate:"required"` } - // 更新密碼請求 - UpdateTokenReq { - Account string `json:"account" validate:"required,min=3,max=50"` - Token string `json:"token" validate:"required,min=8,max=128"` - Platform int `json:"platform" validate:"required,oneof=1 2 3 4"` - } - - // 生成驗證碼請求 - GenerateRefreshCodeReq { - Account string `json:"account" validate:"required,min=3,max=50"` - CodeType int `json:"code_type" validate:"required,oneof=1 2 3"` - } - - // 驗證碼響應 - VerifyCodeResp { - VerifyCode string `json:"verify_code"` - } - - // 生成驗證碼響應 - GenerateRefreshCodeResp { - Data VerifyCodeResp `json:"data"` - } - - // 驗證碼請求 - VerifyRefreshCodeReq { - Account string `json:"account" validate:"required,min=3,max=50"` - CodeType int `json:"code_type" validate:"required,oneof=1 2 3"` - VerifyCode string `json:"verify_code" validate:"required,min=4,max=10"` - } - - // 更新狀態請求 - UpdateStatusReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Status int `json:"status" validate:"required,oneof=0 1 2 3"` - } - - // 獲取用戶資訊請求 - GetUserInfoReq { - UID string `json:"uid,omitempty" validate:"omitempty,min=1,max=50"` - NickName *string `json:"nick_name,omitempty" validate:"omitempty,min=1,max=50"` - } - - // 用戶資訊 - UserInfo { - UID string `json:"uid"` - AvatarURL *string `json:"avatar_url,omitempty"` - FullName *string `json:"full_name,omitempty"` - NickName *string `json:"nick_name,omitempty"` - GenderCode *int64 `json:"gender_code,omitempty"` - Birthday *int64 `json:"birthday,omitempty"` - Phone *string `json:"phone,omitempty"` - Email *string `json:"email,omitempty"` - Address *string `json:"address,omitempty"` - AlarmType int `json:"alarm_type"` - Status int `json:"status"` - Language string `json:"language"` - Currency string `json:"currency"` - CreateTime int64 `json:"create_time"` - UpdateTime int64 `json:"update_time"` - } - - // 獲取用戶資訊響應 - GetUserInfoResp { - Data UserInfo `json:"data"` - } - - // 用戶列表請求 - ListUserInfoReq { - AlarmType *int `json:"alarm_type,omitempty" validate:"omitempty,oneof=0 1 2"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - CreateStartTime *int64 `json:"create_start_time,omitempty"` - CreateEndTime *int64 `json:"create_end_time,omitempty"` - PageSize int `json:"page_size" validate:"required,min=1,max=100"` - PageIndex int `json:"page_index" validate:"required,min=1"` - } - - // 用戶列表響應 - ListUserInfoResp { - Data []UserInfo `json:"data"` - Pager PagerResp `json:"pager"` - } - - // 驗證認證結果請求 - VerifyAuthResultReq { - Token string `json:"token" validate:"required,min=1"` - Account *string `json:"account,omitempty" validate:"omitempty,min=3,max=50"` - } - - // 驗證認證結果響應 - VerifyAuthResultResp { - Status bool `json:"status"` - } - - // Google 認證結果響應 - VerifyGoogleAuthResultResp { - Status bool `json:"status"` - Iss *string `json:"iss,omitempty"` - Sub *string `json:"sub,omitempty"` - Aud *string `json:"aud,omitempty"` - Exp *string `json:"exp,omitempty"` - Iat *string `json:"iat,omitempty"` - Email *string `json:"email,omitempty"` - EmailVerified *string `json:"email_verified,omitempty"` - Name *string `json:"name,omitempty"` - Picture *string `json:"picture,omitempty"` - } - - // 綁定驗證 Email 請求 - BindVerifyEmailReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Email string `json:"email" validate:"required,email"` - } - - // 綁定驗證 Phone 請求 - BindVerifyPhoneReq { - UID string `json:"uid" validate:"required,min=1,max=50"` - Phone string `json:"phone" validate:"required,min=10,max=20"` - } - - // LINE 獲取 Token 請求 - LineGetTokenReq { - Code string `json:"code" validate:"required,min=1"` - } - - // LINE Access Token 響應 - LineAccessTokenResp { - Token string `json:"token"` - } - - // LINE 用戶資料 - LineUserProfile { - DisplayName string `json:"display_name"` - UserID string `json:"user_id"` - PictureURL string `json:"picture_url"` - StatusMessage string `json:"status_message"` - } - - // LINE 獲取用戶資訊請求 - LineGetUserInfoReq { - Token string `json:"token" validate:"required,min=1"` + // RefreshTokenResp 刷新權杖後的響應 + RefreshTokenResp { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` // 可選:某些策略下刷新後也會換發新的 Refresh Token + TokenType string `json:"token_type"` } ) -// ================ API 路由 ================ + +// ================================================================= +// Service: Member (公開 API - 無需登入) +// Group: account +// ================================================================= + @server( group: account prefix: /api/v1/account + schemes: https + timeout: 10s ) service gateway { + // =================================== + // 1. 註冊 (Register) + // =================================== @doc( - summary: "創建用戶帳號" - description: "創建新的用戶帳號,支援多平台登入" + summary: "註冊新帳號" + description: "使用傳統帳號密碼或第三方平台進行註冊。成功後直接返回登入後的 Token 資訊。" ) -/* - @respdoc-200 (OKResp) // 創建成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40002: (ErrorResp) 密碼強度不足 - 40003: (ErrorResp) 平台類型無效 - ) // 客戶端錯誤 - @respdoc-409 (ErrorResp) // 帳號已存在 - @respdoc-422 (ErrorResp) // 請求格式正確但語義錯誤 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler CreateUserAccount - post /create (CreateUserAccountReq) returns (OKResp) + /* + @respdoc-200 (LoginResp) // 註冊成功,並返回 Token + @respdoc-400 ( + 40001: (ErrorResp) "請求參數格式錯誤" + 40002: (ErrorResp) "密碼與確認密碼不一致" + 40003: (ErrorResp) "無效的認證方式或平台" + 40004: (ErrorResp) "無效的平台 Token" + ) + @respdoc-409 (ErrorResp) // 409 Conflict: 代表資源衝突,這裡表示帳號已存在 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler register + post /register (RegisterReq) returns (LoginResp) + + + // =================================== + // 2. 登入 (Login / Create Session) + // =================================== + @doc( + summary: "使用者登入" + description: "使用傳統帳號密碼或第三方平台 Token 進行登入,以創建一個新的會話(Session)。" + ) + /* + @respdoc-200 (LoginResp) // 登入成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 ( + 40101: (ErrorResp) "帳號或密碼錯誤" + 40102: (ErrorResp) "無效的平台 Token" + ) // 401 Unauthorized: 代表身份驗證失敗 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler login + post /sessions/login (RegisterReq) returns (LoginResp) + + // =================================== + // 3. 權杖刷新 (Token Refresh) + // =================================== + @doc( + summary: "刷新 Access Token" + description: "使用有效的 Refresh Token 來獲取一組新的 Access Token 和 Refresh Token。" + ) + /* + @respdoc-200 (RefreshTokenResp) // 刷新成功 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "無效或已過期的 Refresh Token" // 401 Unauthorized + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler refreshToken + post /sessions/refresh (RefreshTokenReq) returns (RefreshTokenResp) + + // =================================== + // 4. 密碼重設 (Password Reset) + // =================================== + @doc( + summary: "請求發送密碼重設驗證碼(忘記密碼)" + description: "為指定的 email 或 phone 發送一個一次性的密碼重設驗證碼。三分鐘內對同一帳號只能請求一次。" + ) + /* + @respdoc-201 () // 請求成功 (為安全起見,即使帳號不存在也應返回成功) + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-429 (ErrorResp) // 429 Too Many Requests: 代表請求過於頻繁 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler requestPasswordReset + post /password-resets/request (RequestPasswordResetReq) returns () + @doc( - summary: "獲取帳號資訊" - description: "根據帳號獲取用戶的帳號資訊" + summary: "校驗密碼重設驗證碼" + description: "在實際重設密碼前,先驗證使用者輸入的驗證碼是否正確。這一步可以讓前端在使用者進入下一步前就得到反饋。" ) -/* - @respdoc-200 (GetAccountInfoResp) // 獲取成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUserAccountInfo - post /info (GetUIDByAccountReq) returns (GetAccountInfoResp) + /* + @respdoc-200 (OKResp) // 驗證碼正確 + @respdoc-400 ( + 40001: (ErrorResp) "請求參數格式錯誤" + 40002: (ErrorResp) "驗證碼無效或已過期" + ) + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler verifyPasswordResetCode + post /password-resets/verify (VerifyCodeReq) returns (OKResp) @doc( - summary: "更新用戶密碼" - description: "更新指定帳號的密碼" + summary: "執行密碼重設" + description: "使用有效的驗證碼來設定新的密碼。" ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40002: (ErrorResp) 密碼強度不足 - 40003: (ErrorResp) 平台類型無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateUserToken - post /update-token (UpdateTokenReq) returns (OKResp) - - @doc( - summary: "獲取用戶 UID" - description: "根據帳號獲取對應的用戶 UID" - ) -/* - @respdoc-200 (GetUIDByAccountResp) // 獲取成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUIDByAccount - post /uid (GetUIDByAccountReq) returns (GetUIDByAccountResp) - - @doc( - summary: "綁定帳號" - description: "將帳號綁定到指定的用戶 UID" - ) -/* - @respdoc-200 (BindingUserResp) // 綁定成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-409 (ErrorResp) // 帳號已綁定 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindAccount - post /bind (BindingUserReq) returns (BindingUserResp) - - @doc( - summary: "綁定用戶資料" - description: "初次綁定用戶的詳細資料" - ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 用戶資料已存在 - @respdoc-422 (ErrorResp) // 用戶資料不完整 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindUserInfo - post /bind-info (CreateUserInfoReq) returns (OKResp) - - @doc( - summary: "綁定驗證 Email" - description: "綁定並驗證用戶的 Email 地址" - ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 郵箱已綁定 - @respdoc-422 (ErrorResp) // 郵箱未驗證 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindVerifyEmail - post /bind-email (BindVerifyEmailReq) returns (OKResp) - - @doc( - summary: "綁定驗證 Phone" - description: "綁定並驗證用戶的電話號碼" - ) -/* - @respdoc-200 (OKResp) // 綁定成功 - @respdoc-400 ( - 40005: (ErrorResp) UID 格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-409 (ErrorResp) // 電話已綁定 - @respdoc-422 (ErrorResp) // 電話未驗證 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler BindVerifyPhone - post /bind-phone (BindVerifyPhoneReq) returns (OKResp) - - @doc( - summary: "更新用戶資料" - description: "更新用戶的個人資料資訊" - ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40006: (ErrorResp) 郵箱格式錯誤 - 40007: (ErrorResp) 電話格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-422 (ErrorResp) // 用戶資料無效 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateUserInfo - post /update-info (UpdateUserInfoReq) returns (OKResp) - - @doc( - summary: "更新用戶狀態" - description: "更新用戶的帳號狀態" - ) -/* - @respdoc-200 (OKResp) // 更新成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - 40008: (ErrorResp) 狀態值無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-422 (ErrorResp) // 狀態更新無效 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler UpdateStatus - post /update-status (UpdateStatusReq) returns (OKResp) - - @doc( - summary: "獲取用戶資訊" - description: "根據 UID 或暱稱獲取用戶詳細資訊" - ) -/* - @respdoc-200 (GetUserInfoResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40005: (ErrorResp) UID 格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 用戶不存在 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GetUserInfo - post /user-info (GetUserInfoReq) returns (GetUserInfoResp) - - @doc( - summary: "獲取用戶列表" - description: "分頁獲取用戶列表,支援篩選條件" - ) -/* - @respdoc-200 (ListUserInfoResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40009: (ErrorResp) 分頁參數無效 - ) // 客戶端錯誤 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler ListMember - post /list (ListUserInfoReq) returns (ListUserInfoResp) - - @doc( - summary: "生成驗證碼" - description: "為指定帳號生成驗證碼,用於忘記密碼等功能" - ) -/* - @respdoc-200 (GenerateRefreshCodeResp) // 生成成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 帳號不存在 - @respdoc-429 (ErrorResp) // 請求過於頻繁 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler GenerateRefreshCode - post /generate-code (GenerateRefreshCodeReq) returns (GenerateRefreshCodeResp) - - @doc( - summary: "驗證驗證碼" - description: "驗證並使用驗證碼,驗證後會刪除驗證碼" - ) -/* - @respdoc-200 (OKResp) // 驗證成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - 40011: (ErrorResp) 驗證碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 驗證碼不存在 - @respdoc-422 (ErrorResp) // 驗證碼無效或已過期 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyRefreshCode - post /verify-code (VerifyRefreshCodeReq) returns (OKResp) - - @doc( - summary: "檢查驗證碼" - description: "檢查驗證碼是否正確,但不刪除驗證碼" - ) -/* - @respdoc-200 (OKResp) // 檢查成功 - @respdoc-400 ( - 40001: (ErrorResp) 帳號格式錯誤 - 40004: (ErrorResp) 參數驗證失敗 - 40010: (ErrorResp) 驗證碼類型無效 - 40011: (ErrorResp) 驗證碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-404 (ErrorResp) // 驗證碼不存在 - @respdoc-422 (ErrorResp) // 驗證碼無效或已過期 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler CheckRefreshCode - post /check-code (VerifyRefreshCodeReq) returns (OKResp) - - @doc( - summary: "驗證 Google 認證" - description: "驗證 Google OAuth 登入是否有效" - ) -/* - @respdoc-200 (VerifyGoogleAuthResultResp) // 驗證成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // Google 認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyGoogleAuthResult - post /verify-google (VerifyAuthResultReq) returns (VerifyGoogleAuthResultResp) - - @doc( - summary: "驗證平台認證" - description: "驗證平台登入認證是否有效" - ) -/* - @respdoc-200 (VerifyAuthResultResp) // 驗證成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // 平台認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler VerifyPlatformAuthResult - post /verify-platform (VerifyAuthResultReq) returns (VerifyAuthResultResp) - - @doc( - summary: "LINE 獲取 Access Token" - description: "使用 LINE 授權碼獲取 Access Token" - ) -/* - @respdoc-200 (LineAccessTokenResp) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40013: (ErrorResp) 授權碼格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // 授權碼無效或過期 - @respdoc-422 (ErrorResp) // LINE 認證失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler LineCodeToAccessToken - post /line/token (LineGetTokenReq) returns (LineAccessTokenResp) - - @doc( - summary: "LINE 獲取用戶資料" - description: "使用 LINE Access Token 獲取用戶資料" - ) -/* - @respdoc-200 (LineUserProfile) // 獲取成功 - @respdoc-400 ( - 40004: (ErrorResp) 參數驗證失敗 - 40012: (ErrorResp) Token 格式錯誤 - ) // 客戶端錯誤 - @respdoc-401 (ErrorResp) // Token 無效或過期 - @respdoc-422 (ErrorResp) // LINE 用戶資料獲取失敗 - @respdoc-500 (ErrorResp) // 服務器錯誤 -*/ - @handler LineGetProfileByAccessToken - post /line/profile (LineGetUserInfoReq) returns (LineUserProfile) + /* + @respdoc-201 () // 密碼重設成功 + @respdoc-400 ( + 40001: (ErrorResp) "請求參數格式錯誤" + 40002: (ErrorResp) "密碼與確認密碼不一致" + 40003: (ErrorResp) "驗證碼無效或已過期" + ) + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler resetPassword + put /password-resets (ResetPasswordReq) returns () } + + +// ================ 請求/響應結構 ================ +type ( + // --- 會員資訊 --- + + // UserInfoResp 用於獲取會員資訊的標準響應結構 (合併 GetUserInfo 和 UserInfo) + UserInfoResp { + Platform string `json:"platform"` // 註冊平台 + UID string `json:"uid"` // 用戶 UID + AvatarURL string `json:"avatar_url"` // 頭像 URL + FullName string `json:"full_name"` // 用戶全名 + Nickname string `json:"nickname"` // 暱稱 + GenderCode string `json:"gender_code"` // 性別代碼 + Birthdate string `json:"birthdate"` // 生日 (格式: 1993-04-17) + PhoneNumber string `json:"phone_number"` // 電話 + IsPhoneVerified bool `json:"is_phone_verified"` // 手機是否已驗證 + Email string `json:"email"` // 信箱 + IsEmailVerified bool `json:"is_email_verified"` // 信箱是否已驗證 + Address string `json:"address"` // 地址 + AlarmCategory string `json:"alarm_category"` // 告警狀態 + UserStatus string `json:"user_status"` // 用戶狀態 + PreferredLanguage string `json:"preferred_language"` // 偏好語言 + Currency string `json:"currency"` // 偏好幣種 + National string `json:"national"` // 國家 + PostCode string `json:"post_code"` // 郵遞區號 + Carrier string `json:"carrier"` // 載具 + Role string `json:"role"` // 角色 + } + + // UpdateUserInfoReq 更新會員資訊的請求結構 (原 BindingUserInfo) + UpdateUserInfoReq { + AvatarURL *string `json:"avatar_url,optional"` // 頭像 URL + FullName *string `json:"full_name,optional"` // 用戶全名 + Nickname *string `json:"nickname,optional"` // 暱稱 + GenderCode *string `json:"gender_code,optional" validate:"omitempty,oneof=secret male female"` + Birthdate *string `json:"birthdate,optional"` // 生日 (格式: 1993-04-17) + Address *string `json:"address,optional"` // 地址 + PreferredLanguage *string `json:"preferred_language,optional" validate:"omitempty,oneof=zh-tw en-us"` + Currency *string `json:"currency,optional" validate:"omitempty,oneof=TWD USD"` + National *string `json:"national,optional"` // 國家 + PostCode *string `json:"post_code,optional"` // 郵遞區號 + Carrier *string `json:"carrier,optional"` // 載具 + VerifyHeader + } + + // --- 修改密碼 --- + + // UpdatePasswordReq 修改密碼的請求 (原 ModifyPasswdReq) + UpdatePasswordReq { + CurrentPassword string `json:"current_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=8,max=128"` + NewPasswordConfirm string `json:"new_password_confirm" validate:"eqfield=NewPassword"` + VerifyHeader + } + + // --- 通用驗證碼 --- + + // RequestVerificationCodeReq 請求發送驗證碼 + RequestVerificationCodeReq { + VerifyHeader + // 驗證目的:'email_verification' 或 'phone_verification' + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + } + + // SubmitVerificationCodeReq 提交驗證碼以完成驗證 + SubmitVerificationCodeReq { + Purpose string `json:"purpose" validate:"required,oneof=email_verification phone_verification"` + VerifyCode string `json:"verify_code" validate:"required,len=6"` + VerifyHeader + } +) + +// ================================================================= +// Service: Gateway (授權 API - 需要登入) +// Group: user +// Middleware: AuthMiddleware (JWT 驗證) +// ================================================================= + +@server( + group: user + prefix: /api/v1/user + schemes: https + timeout: 10s + middleware: AuthMiddleware +) +service gateway { + // =================================== + // 1. 會員資訊 (User Profile) + // =================================== + @doc( + summary: "取得當前登入的會員資訊" + ) + /* + @respdoc-200 (UserInfoResp) // 成功獲取 + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-404 (ErrorResp) "找不到使用者" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler getUserInfo + + get /me (VerifyHeader) returns (UserInfoResp) + + @doc( + summary: "更新當前登入的會員資訊" + description: "只更新傳入的欄位,未傳入的欄位將保持不變。" + ) + /* + @respdoc-200 (UserInfoResp) // 更新成功,並返回更新後的使用者資訊 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updateUserInfo + + put /me (UpdateUserInfoReq) returns (UserInfoResp) + + // =================================== + // 2. 修改密碼 (Password Update) + // =================================== + @doc( + summary: "修改當前登入使用者的密碼" + description: "必須提供當前密碼以進行驗證。" + ) + /* + @respdoc-200 (OKResp) // 密碼修改成功 + @respdoc-400 ( + 40001: (ErrorResp) "請求參數格式錯誤" + 40002: (ErrorResp) "新密碼與確認密碼不一致" + ) + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-403 (ErrorResp) "當前密碼不正確" // 403 Forbidden: 認證成功,但無權執行此操作 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler updatePassword + put /me/password (UpdatePasswordReq) returns (OKResp) + + // =================================== + // 3. 驗證碼流程 (Verification Flow) + // =================================== + @doc( + summary: "請求發送驗證碼" + description: "用於驗證手機或 Email。根據傳入的 `purpose` 發送對應的驗證碼。同一個目的在短時間內不能重複發送。" + ) + /* + @respdoc-200 (OKResp) // 請求已受理 + @respdoc-400 (ErrorResp) "請求參數格式錯誤" + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-429 (ErrorResp) // 429 Too Many Requests: 請求過於頻繁 + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler requestVerificationCode + post /me/verifications (RequestVerificationCodeReq) returns (OKResp) + + @doc( + summary: "提交驗證碼以完成驗證" + description: "提交收到的驗證碼,以完成特定目的的驗證,例如綁定手機或 Email。" + ) + /* + @respdoc-200 (OKResp) // 驗證成功 + @respdoc-400 ( + 40001: (ErrorResp) "請求參數格式錯誤" + 40002: (ErrorResp) "驗證碼無效或已過期" + ) + @respdoc-401 (ErrorResp) "未授權或 Token 無效" + @respdoc-500 (ErrorResp) // 伺服器內部錯誤 + */ + @handler submitVerificationCode + put /me/verifications (SubmitVerificationCodeReq) returns (OKResp) +} \ No newline at end of file diff --git a/example/test_all_params.api b/example/test_all_params.api new file mode 100644 index 0000000..0ba0379 --- /dev/null +++ b/example/test_all_params.api @@ -0,0 +1,51 @@ +syntax = "v1" + +info ( + title: "Test All Parameter Types" + version: "v1" +) + +type ( + // 同時包含所有類型的參數 + AllParamsReq { + // Header 參數 + Authorization string `header:"Authorization" validate:"required"` + XRequestID string `header:"X-Request-ID,optional"` + + // Path 參數 + UserID string `path:"userId" validate:"required"` + ItemID string `path:"itemId" validate:"required"` + + // Query 參數 (form) + Page int `form:"page,optional"` + PageSize int `form:"pageSize,optional"` + Filter string `form:"filter,optional"` + + // JSON Body 參數 + Name string `json:"name" validate:"required"` + Description string `json:"description,optional"` + Tags []string `json:"tags,optional"` + } + + Response { + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + } +) + +@server ( + prefix: /api/v1 +) +service test { + // 測試所有參數類型同時存在 + @handler UpdateWithAllParams + put /users/:userId/items/:itemId (AllParamsReq) returns (Response) + + // 測試只有 path + query + @handler GetWithPathQuery + get /users/:userId/items/:itemId (AllParamsReq) returns (Response) + + // 測試 path + header + body (POST) + @handler CreateWithPathHeaderBody + post /users/:userId/items (AllParamsReq) returns (Response) +} diff --git a/example/test_header_body.api b/example/test_header_body.api new file mode 100644 index 0000000..fe7336c --- /dev/null +++ b/example/test_header_body.api @@ -0,0 +1,41 @@ +syntax = "v1" + +info ( + title: "Test Header with Body" + version: "v1" +) + +type ( + HeaderDef { + Token string `header:"Authorization" validate:"required"` + } + + // 方式1: 嵌入 header 結構體 + UpdateReqWithEmbed { + Name string `json:"name"` + Age int `json:"age"` + HeaderDef + } + + // 方式2: 分開定義 + UpdateReqSeparate { + Token string `header:"Authorization" validate:"required"` + Name string `json:"name"` + Age int `json:"age"` + } + + Response { + Message string `json:"message"` + } +) + +@server ( + prefix: /api/v1 +) +service test { + @handler TestWithEmbed + put /with-embed (UpdateReqWithEmbed) returns (Response) + + @handler TestSeparate + put /separate (UpdateReqSeparate) returns (Response) +} diff --git a/example/test_header_path.api b/example/test_header_path.api new file mode 100644 index 0000000..12f42dc --- /dev/null +++ b/example/test_header_path.api @@ -0,0 +1,59 @@ +syntax = "v1" + +info ( + title: "Test Header and Path Parameters" + version: "v1" +) + +type ( + HeaderReq { + Token string `header:"Authorization" validate:"required"` + } + + PathReq { + ID string `path:"id" validate:"required"` + } + + QueryReq { + Name string `form:"name,optional"` + } + + BodyReq { + Data string `json:"data"` + } + + Response { + Message string `json:"message"` + } + + CombinedReq { + Token string `header:"Authorization" validate:"required"` + ID string `path:"id" validate:"required"` + Data string `json:"data"` + } +) + +@server ( + prefix: /api/v1 +) +service test { + // Test header parameter + @handler TestHeader + get /header (HeaderReq) returns (Response) + + // Test path parameter + @handler TestPath + get /path/:id (PathReq) returns (Response) + + // Test query parameter + @handler TestQuery + get /query (QueryReq) returns (Response) + + // Test body parameter + @handler TestBody + post /body (BodyReq) returns (Response) + + // Test combined + @handler TestCombined + post /combined/:id (CombinedReq) returns (Response) +} diff --git a/internal/swagger/const.go b/internal/swagger/const.go index 318ba56..f2e81a6 100644 --- a/internal/swagger/const.go +++ b/internal/swagger/const.go @@ -50,16 +50,16 @@ const ( propertyKeyTags = "tags" propertyKeySummary = "summary" propertyKeyGroup = "group" - propertyKeyOperationId = "operationId" - propertyKeyDeprecated = "deprecated" - propertyKeyPrefix = "prefix" - propertyKeyAuthType = "authType" - propertyKeyHost = "host" - propertyKeyBasePath = "basePath" - propertyKeyWrapCodeMsg = "wrapCodeMsg" - propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription" + // propertyKeyOperationId = "operationId" + propertyKeyDeprecated = "deprecated" + propertyKeyPrefix = "prefix" + propertyKeyAuthType = "authType" + propertyKeyHost = "host" + propertyKeyBasePath = "basePath" + propertyKeyWrapCodeMsg = "wrapCodeMsg" + propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription" ) const ( - defaultValueOfPropertyUseDefinition = false + defaultValueOfPropertyUseDefinition = true ) diff --git a/internal/swagger/parameter.go b/internal/swagger/parameter.go index 1683161..bdd8fa4 100644 --- a/internal/swagger/parameter.go +++ b/internal/swagger/parameter.go @@ -9,21 +9,24 @@ import ( ) func isPostJson(ctx Context, method string, tp apiSpec.Type) (string, bool) { - if !strings.EqualFold(method, http.MethodPost) { + // Check if this is a method that supports request body (POST, PUT, PATCH) + if !strings.EqualFold(method, http.MethodPost) && + !strings.EqualFold(method, http.MethodPut) && + !strings.EqualFold(method, http.MethodPatch) { return "", false } structType, ok := tp.(apiSpec.DefineStruct) if !ok { return "", false } - var isPostJson bool + var hasJsonField bool rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) { jsonTag, _ := tag.Get(tagJson) - if !isPostJson { - isPostJson = jsonTag != nil + if !hasJsonField { + hasJsonField = jsonTag != nil } }) - return structType.RawName, isPostJson + return structType.RawName, hasJsonField } func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Parameter { diff --git a/internal/swagger/swagger.go b/internal/swagger/swagger.go index 9e1405d..807f351 100644 --- a/internal/swagger/swagger.go +++ b/internal/swagger/swagger.go @@ -211,12 +211,17 @@ func rangeMemberAndDo(ctx Context, structType apiSpec.Type, do func(tag *apiSpec for _, field := range members { tags, _ := apiSpec.Parse(field.Tag) - required := isRequired(ctx, tags) + required := isRequired(ctx, tags, field) do(tags, required, field) } } -func isRequired(ctx Context, tags *apiSpec.Tags) bool { +func isRequired(ctx Context, tags *apiSpec.Tags, member apiSpec.Member) bool { + // Check if the field type is a pointer - pointer types are optional + if _, isPointer := member.Type.(apiSpec.PointerType); isPointer { + return false + } + tag, err := tags.Get(tagJson) if err == nil { return !isOptional(ctx, tag.Options) @@ -234,7 +239,7 @@ func isRequired(ctx Context, tags *apiSpec.Tags) bool { func isOptional(_ Context, options []string) bool { for _, option := range options { - if option == optionalFlag { + if option == optionalFlag || option == "omitempty" { return true } }