[app-server] remove serde(skip_serializing_if = "Option::is_none") annotations (#5939)

We had this annotation everywhere in app-server APIs which made it so
that fields get serialized as `field?: T`, meaning if the field as
`None` we would omit the field in the payload. Removing this annotation
changes it so that we return `field: T | null` instead, which makes
codex app-server's API more aligned with the convention of public OpenAI
APIs like Responses.

Separately, remove the `#[ts(optional_fields = nullable)]` annotations
that were recently added which made all the TS types become `field?: T |
null` which is not great since clients need to handle undefined and
null.

I think generally it'll be best to have optional types be either:
- `field: T | null` (preferred, aligned with public OpenAI APIs)
- `field?: T` where we have to, such as types generated from the MCP
schema:
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts
(see changes to `mcp-types/`)

I updated @etraut-openai's unit test to check that all generated TS
types are one or the other, not both (so will error if we have a type
that has `field?: T | null`). I don't think there's currently a good use
case for that - but we can always revisit.
This commit is contained in:
Owen Lin
2025-10-30 11:18:53 -07:00
committed by GitHub
parent 9572cfc782
commit 89c00611c2
13 changed files with 265 additions and 193 deletions

View File

@@ -61,7 +61,6 @@ impl SandboxRiskCategory {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ExecApprovalRequestEvent {
/// Identifier for the associated exec call, if available.
pub call_id: String,
@@ -79,7 +78,6 @@ pub struct ExecApprovalRequestEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ApplyPatchApprovalRequestEvent {
/// Responses API call id for the associated patch apply call, if available.
pub call_id: String,

View File

@@ -11,7 +11,6 @@ use ts_rs::TS;
pub const PROMPTS_CMD_PREFIX: &str = "prompts";
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct CustomPrompt {
pub name: String,
pub path: PathBuf,

View File

@@ -48,36 +48,35 @@ pub enum ContentItem {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseItem {
Message {
#[serde(skip_serializing)]
#[ts(optional = nullable)]
#[serde(default, skip_serializing)]
#[ts(skip)]
id: Option<String>,
role: String,
content: Vec<ContentItem>,
},
Reasoning {
#[serde(default, skip_serializing)]
#[ts(skip)]
id: String,
summary: Vec<ReasoningItemReasoningSummary>,
#[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
#[ts(optional = nullable)]
#[ts(optional)]
content: Option<Vec<ReasoningItemContent>>,
#[ts(optional = nullable)]
encrypted_content: Option<String>,
},
LocalShellCall {
/// Set when using the chat completions API.
#[serde(skip_serializing)]
#[ts(optional = nullable)]
#[serde(default, skip_serializing)]
#[ts(skip)]
id: Option<String>,
/// Set when using the Responses API.
#[ts(optional = nullable)]
call_id: Option<String>,
status: LocalShellStatus,
action: LocalShellAction,
},
FunctionCall {
#[serde(skip_serializing)]
#[ts(optional = nullable)]
#[serde(default, skip_serializing)]
#[ts(skip)]
id: Option<String>,
name: String,
// The Responses API returns the function call arguments as a *string* that contains
@@ -97,11 +96,11 @@ pub enum ResponseItem {
output: FunctionCallOutputPayload,
},
CustomToolCall {
#[serde(skip_serializing)]
#[ts(optional = nullable)]
#[serde(default, skip_serializing)]
#[ts(skip)]
id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
#[ts(optional)]
status: Option<String>,
call_id: String,
@@ -121,11 +120,11 @@ pub enum ResponseItem {
// "action": {"type":"search","query":"weather: San Francisco, CA"}
// }
WebSearchCall {
#[serde(skip_serializing)]
#[ts(optional = nullable)]
#[serde(default, skip_serializing)]
#[ts(skip)]
id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
#[ts(optional)]
status: Option<String>,
action: WebSearchAction,
},
@@ -203,7 +202,6 @@ pub enum LocalShellAction {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct LocalShellExecAction {
pub command: Vec<String>,
pub timeout_ms: Option<u64>,
@@ -296,7 +294,6 @@ impl From<Vec<UserInput>> for ResponseInputItem {
/// If the `name` of a `ResponseItem::FunctionCall` is either `container.exec`
/// or shell`, the `arguments` field should deserialize to this struct.
#[derive(Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ShellToolCallParams {
pub command: Vec<String>,
pub workdir: Option<String>,
@@ -329,7 +326,6 @@ pub enum FunctionCallOutputContentItem {
/// `content_items` with the structured form that the Responses/Chat
/// Completions APIs understand.
#[derive(Debug, Default, Clone, PartialEq, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct FunctionCallOutputPayload {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -18,14 +18,11 @@ pub enum ParsedCommand {
},
ListFiles {
cmd: String,
#[ts(optional = nullable)]
path: Option<String>,
},
Search {
cmd: String,
#[ts(optional = nullable)]
query: Option<String>,
#[ts(optional = nullable)]
path: Option<String>,
},
Unknown {

View File

@@ -21,7 +21,6 @@ pub struct PlanItemArg {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)]
#[serde(deny_unknown_fields)]
#[ts(optional_fields = nullable)]
pub struct UpdatePlanArgs {
#[serde(default)]
pub explanation: Option<String>,

View File

@@ -661,7 +661,6 @@ impl HasLegacyEvent for EventMsg {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct ExitedReviewModeEvent {
pub review_output: Option<ReviewOutputEvent>,
}
@@ -674,13 +673,11 @@ pub struct ErrorEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TaskCompleteEvent {
pub last_agent_message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TaskStartedEvent {
pub model_context_window: Option<i64>,
}
@@ -700,11 +697,9 @@ pub struct TokenUsage {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage,
#[ts(optional = nullable)]
#[ts(type = "number | null")]
pub model_context_window: Option<i64>,
}
@@ -765,30 +760,25 @@ impl TokenUsageInfo {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TokenCountEvent {
pub info: Option<TokenUsageInfo>,
pub rate_limits: Option<RateLimitSnapshot>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct RateLimitWindow {
/// Percentage (0-100) of the window that has been consumed.
pub used_percent: f64,
/// Rolling window duration, in minutes.
#[ts(optional = nullable)]
#[ts(type = "number | null")]
pub window_minutes: Option<i64>,
/// Unix timestamp (seconds since epoch) when the window resets.
#[ts(optional = nullable)]
#[ts(type = "number | null")]
pub resets_at: Option<i64>,
}
@@ -902,7 +892,6 @@ pub struct AgentMessageEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UserMessageEvent {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -938,7 +927,6 @@ pub struct AgentReasoningDeltaEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
#[ts(optional_fields = nullable)]
pub struct McpInvocation {
/// Name of the MCP server as defined in the config.
pub server: String,
@@ -1067,7 +1055,6 @@ pub enum SubAgentSource {
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionMeta {
pub id: ConversationId,
pub timestamp: String,
@@ -1096,7 +1083,6 @@ impl Default for SessionMeta {
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionMetaLine {
#[serde(flatten)]
pub meta: SessionMeta,
@@ -1132,7 +1118,6 @@ impl From<CompactedItem> for ResponseItem {
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct TurnContextItem {
pub cwd: PathBuf,
pub approval_policy: AskForApproval,
@@ -1151,7 +1136,6 @@ pub struct RolloutLine {
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct GitInfo {
/// Current commit hash (SHA)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1285,7 +1269,6 @@ pub struct BackgroundEventEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct DeprecationNoticeEvent {
/// Concise summary of what is deprecated.
pub summary: String,
@@ -1295,14 +1278,12 @@ pub struct DeprecationNoticeEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UndoStartedEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct UndoCompletedEvent {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1347,7 +1328,6 @@ pub struct TurnDiffEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct GetHistoryEntryResponseEvent {
pub offset: usize,
pub log_id: u64,
@@ -1397,7 +1377,6 @@ pub struct ListCustomPromptsResponseEvent {
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[ts(optional_fields = nullable)]
pub struct SessionConfiguredEvent {
/// Name left as session_id instead of conversation_id for backwards compatibility.
pub session_id: ConversationId,
@@ -1458,7 +1437,6 @@ pub enum FileChange {
},
Update {
unified_diff: String,
#[ts(optional = nullable)]
move_path: Option<PathBuf>,
},
}