diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs
index 2fff3a427745..c8facffdb599 100644
--- a/codex-rs/app-server-protocol/src/export.rs
+++ b/codex-rs/app-server-protocol/src/export.rs
@@ -545,7 +545,7 @@ mod tests {
     use uuid::Uuid;
 
     #[test]
-    fn generated_ts_omits_undefined_unions_for_optionals() -> Result<()> {
+    fn generated_ts_has_no_optional_nullable_fields() -> Result<()> {
         let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7()));
         fs::create_dir(&output_dir)?;
 
@@ -562,7 +562,7 @@ mod tests {
         generate_ts(&output_dir, None)?;
 
         let mut undefined_offenders = Vec::new();
-        let mut missing_optional_marker = BTreeSet::new();
+        let mut optional_nullable_offenders = BTreeSet::new();
         let mut stack = vec![output_dir];
         while let Some(dir) = stack.pop() {
             for entry in fs::read_dir(&dir)? {
@@ -591,27 +591,80 @@ mod tests {
                     let mut search_start = 0;
                     while let Some(idx) = contents[search_start..].find("| null") {
                         let abs_idx = search_start + idx;
-                        let Some(colon_idx) = contents[..abs_idx].rfind(':') else {
-                            search_start = abs_idx + 5;
-                            continue;
-                        };
+                        // Find the property-colon for this field by scanning forward
+                        // from the start of the segment and ignoring nested braces,
+                        // brackets, and parens. This avoids colons inside nested
+                        // type literals like `{ [k in string]?: string }`.
 
-                        let line_start_idx = contents[..colon_idx]
-                            .rfind('\n')
-                            .map(|i| i + 1)
-                            .unwrap_or(0);
+                        let line_start_idx =
+                            contents[..abs_idx].rfind('\n').map(|i| i + 1).unwrap_or(0);
 
                         let mut segment_start_idx = line_start_idx;
-                        if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind(',') {
+                        if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind(',') {
                             segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1);
                         }
-                        if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind('{') {
+                        if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind('{') {
                             segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1);
                         }
-                        if let Some(rel_idx) = contents[line_start_idx..colon_idx].rfind('}') {
+                        if let Some(rel_idx) = contents[line_start_idx..abs_idx].rfind('}') {
                             segment_start_idx = segment_start_idx.max(line_start_idx + rel_idx + 1);
                         }
 
+                        // Scan forward for the colon that separates the field name from its type.
+                        let mut level_brace = 0_i32;
+                        let mut level_brack = 0_i32;
+                        let mut level_paren = 0_i32;
+                        let mut in_single = false;
+                        let mut in_double = false;
+                        let mut escape = false;
+                        let mut prop_colon_idx = None;
+                        for (i, ch) in contents[segment_start_idx..abs_idx].char_indices() {
+                            let idx_abs = segment_start_idx + i;
+                            if escape {
+                                escape = false;
+                                continue;
+                            }
+                            match ch {
+                                '\\' => {
+                                    // Only treat as escape when inside a string.
+                                    if in_single || in_double {
+                                        escape = true;
+                                    }
+                                }
+                                '\'' => {
+                                    if !in_double {
+                                        in_single = !in_single;
+                                    }
+                                }
+                                '"' => {
+                                    if !in_single {
+                                        in_double = !in_double;
+                                    }
+                                }
+                                '{' if !in_single && !in_double => level_brace += 1,
+                                '}' if !in_single && !in_double => level_brace -= 1,
+                                '[' if !in_single && !in_double => level_brack += 1,
+                                ']' if !in_single && !in_double => level_brack -= 1,
+                                '(' if !in_single && !in_double => level_paren += 1,
+                                ')' if !in_single && !in_double => level_paren -= 1,
+                                ':' if !in_single
+                                    && !in_double
+                                    && level_brace == 0
+                                    && level_brack == 0
+                                    && level_paren == 0 =>
+                                {
+                                    prop_colon_idx = Some(idx_abs);
+                                    break;
+                                }
+                                _ => {}
+                            }
+                        }
+
+                        let Some(colon_idx) = prop_colon_idx else {
+                            search_start = abs_idx + 5;
+                            continue;
+                        };
+
                         let mut field_prefix = contents[segment_start_idx..colon_idx].trim();
                         if field_prefix.is_empty() {
                             search_start = abs_idx + 5;
@@ -640,25 +693,26 @@ mod tests {
                             continue;
                         }
 
+                        // If the last non-whitespace before ':' is '?', then this is an
+                        // optional field with a nullable type (i.e., "?: T | null"),
+                        // which we explicitly disallow.
                         if field_prefix.chars().rev().find(|c| !c.is_whitespace()) == Some('?') {
-                            search_start = abs_idx + 5;
-                            continue;
+                            let line_number =
+                                contents[..abs_idx].chars().filter(|c| *c == '\n').count() + 1;
+                            let offending_line_end = contents[line_start_idx..]
+                                .find('\n')
+                                .map(|i| line_start_idx + i)
+                                .unwrap_or(contents.len());
+                            let offending_snippet =
+                                contents[line_start_idx..offending_line_end].trim();
+
+                            optional_nullable_offenders.insert(format!(
+                                "{}:{}: {offending_snippet}",
+                                path.display(),
+                                line_number
+                            ));
                         }
 
-                        let line_number =
-                            contents[..abs_idx].chars().filter(|c| *c == '\n').count() + 1;
-                        let offending_line_end = contents[line_start_idx..]
-                            .find('\n')
-                            .map(|i| line_start_idx + i)
-                            .unwrap_or(contents.len());
-                        let offending_snippet = contents[line_start_idx..offending_line_end].trim();
-
-                        missing_optional_marker.insert(format!(
-                            "{}:{}: {offending_snippet}",
-                            path.display(),
-                            line_number
-                        ));
-
                         search_start = abs_idx + 5;
                     }
                 }
@@ -670,12 +724,12 @@ mod tests {
             "Generated TypeScript still includes unions with `undefined` in {undefined_offenders:?}"
         );
 
-        // If this test fails, it means that a struct field that is `Option<T>` in Rust
-        // is being generated as `T | null` in TypeScript, without the optional marker
-        // (`?`). To fix this, add #[ts(optional_fields = nullable)] to the struct definition.
+        // If this assertion fails, it means a field was generated as
+        // "?: T | null" — i.e., both optional (undefined) and nullable (null).
+        // We only want either "?: T" or ": T | null".
         assert!(
-            missing_optional_marker.is_empty(),
-            "Generated TypeScript has nullable fields without an optional marker: {missing_optional_marker:?}"
+            optional_nullable_offenders.is_empty(),
+            "Generated TypeScript has optional fields with nullable types (disallowed '?: T | null'), add #[ts(optional)] to fix:\n{optional_nullable_offenders:?}"
         );
 
         Ok(())
diff --git a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs
index 88554176d983..65ffa2a0056d 100644
--- a/codex-rs/app-server-protocol/src/jsonrpc_lite.rs
+++ b/codex-rs/app-server-protocol/src/jsonrpc_lite.rs
@@ -30,20 +30,20 @@ pub enum JSONRPCMessage {
 
 /// A request that expects a response.
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 pub struct JSONRPCRequest {
     pub id: RequestId,
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
 /// A notification which does not expect a response.
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 pub struct JSONRPCNotification {
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
@@ -62,10 +62,10 @@ pub struct JSONRPCError {
 }
 
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 pub struct JSONRPCErrorError {
     pub code: i64,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub data: Option<serde_json::Value>,
     pub message: String,
 }
diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs
index 05c9d91fb270..ca5188f52dfe 100644
--- a/codex-rs/app-server-protocol/src/protocol.rs
+++ b/codex-rs/app-server-protocol/src/protocol.rs
@@ -248,7 +248,6 @@ pub enum Account {
     #[serde(rename = "chatgpt", rename_all = "camelCase")]
     #[ts(rename = "chatgpt", rename_all = "camelCase")]
     ChatGpt {
-        #[ts(optional = nullable)]
         email: Option<String>,
         plan_type: PlanType,
     },
@@ -267,11 +266,9 @@ pub struct InitializeParams {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ClientInfo {
     pub name: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub title: Option<String>,
     pub version: String,
 }
@@ -283,42 +280,33 @@ pub struct InitializeResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct NewConversationParams {
     /// Optional override for the model name (e.g. "o3", "o4-mini").
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model: Option<String>,
 
     /// Override the model provider to use for this session.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model_provider: Option<String>,
 
     /// Configuration profile from config.toml to specify default options.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub profile: Option<String>,
 
     /// Working directory for the session. If relative, it is resolved against
     /// the server process's current working directory.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub cwd: Option<String>,
 
     /// Approval policy for shell commands generated by the model:
     /// `untrusted`, `on-failure`, `on-request`, `never`.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub approval_policy: Option<AskForApproval>,
 
     /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub sandbox: Option<SandboxMode>,
 
     /// Individual config settings that will override what is in
     /// CODEX_HOME/config.toml.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub config: Option<HashMap<String, serde_json::Value>>,
 
     /// The set of instructions to use instead of the default ones.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub base_instructions: Option<String>,
 
     /// Prompt used during conversation compaction.
@@ -326,29 +314,24 @@ pub struct NewConversationParams {
     pub compact_prompt: Option<String>,
 
     /// Whether to include the apply patch tool in the conversation.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub include_apply_patch_tool: Option<bool>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct NewConversationResponse {
     pub conversation_id: ConversationId,
     pub model: String,
     /// Note this could be ignored by the model.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reasoning_effort: Option<ReasoningEffort>,
     pub rollout_path: PathBuf,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ResumeConversationResponse {
     pub conversation_id: ConversationId,
     pub model: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub initial_messages: Option<Vec<EventMsg>>,
     pub rollout_path: PathBuf,
 }
@@ -377,57 +360,46 @@ pub struct GetConversationSummaryResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ListConversationsParams {
     /// Optional page size; defaults to a reasonable server-side value.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub page_size: Option<usize>,
     /// Opaque pagination cursor returned by a previous call.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub cursor: Option<String>,
     /// Optional model provider filter (matches against session metadata).
     /// - None => filter by the server's default model provider
     /// - Some([]) => no filtering, include all providers
     /// - Some([...]) => only include sessions with one of the specified providers
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model_providers: Option<Vec<String>>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ConversationSummary {
     pub conversation_id: ConversationId,
     pub path: PathBuf,
     pub preview: String,
     /// RFC3339 timestamp string for the session start, if available.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub timestamp: Option<String>,
     /// Model provider recorded for the session (resolved when absent in metadata).
     pub model_provider: String,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ListConversationsResponse {
     pub items: Vec<ConversationSummary>,
     /// Opaque cursor to pass to the next call to continue after the last item.
     /// if None, there are no more items to return.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub next_cursor: Option<String>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ListModelsParams {
     /// Optional page size; defaults to a reasonable server-side value.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub page_size: Option<usize>,
     /// Opaque pagination cursor returned by a previous call.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub cursor: Option<String>,
 }
 
@@ -452,24 +424,19 @@ pub struct ReasoningEffortOption {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ListModelsResponse {
     pub items: Vec<Model>,
     /// Opaque cursor to pass to the next call to continue after the last item.
     /// if None, there are no more items to return.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub next_cursor: Option<String>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct UploadFeedbackParams {
     pub classification: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reason: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub conversation_id: Option<ConversationId>,
     pub include_logs: bool,
 }
@@ -497,7 +464,6 @@ pub enum LoginAccountParams {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct LoginAccountResponse {
     /// Only set if the login method is ChatGPT.
@@ -514,20 +480,15 @@ pub struct LoginAccountResponse {
 pub struct LogoutAccountResponse {}
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ResumeConversationParams {
     /// Absolute path to the rollout JSONL file, when explicitly resuming a known rollout.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub path: Option<PathBuf>,
     /// If the rollout path is not known, it can be discovered via the conversation id at the cost of extra latency.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub conversation_id: Option<ConversationId>,
     /// if the rollout path or conversation id is not known, it can be resumed from given history
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub history: Option<Vec<ResponseItem>>,
     /// Optional overrides to apply when spawning the resumed session.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub overrides: Option<NewConversationParams>,
 }
 
@@ -606,19 +567,15 @@ pub struct LogoutChatGptParams {}
 pub struct LogoutChatGptResponse {}
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct GetAuthStatusParams {
     /// If true, include the current auth token (if available) in the response.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub include_token: Option<bool>,
     /// If true, attempt to refresh the token before returning status.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub refresh_token: Option<bool>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ExecOneOffCommandParams {
     /// Command argv to execute.
@@ -627,10 +584,8 @@ pub struct ExecOneOffCommandParams {
     /// If not specified, a sensible default is used server-side.
     pub timeout_ms: Option<u64>,
     /// Optional working directory for the process. Defaults to server config cwd.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub cwd: Option<PathBuf>,
     /// Optional explicit sandbox policy overriding the server default.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub sandbox_policy: Option<SandboxPolicy>,
 }
 
@@ -650,17 +605,13 @@ pub struct GetAccountRateLimitsResponse {
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
 #[serde(rename_all = "camelCase")]
-#[ts(optional_fields = nullable)]
 pub struct GetAuthStatusResponse {
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub auth_method: Option<AuthMode>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub auth_token: Option<String>,
 
     // Indicates that auth method must be valid to use the server.
     // This can be false if using a custom provider that is configured
     // with requires_openai_auth == false.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub requires_openai_auth: Option<bool>,
 }
 
@@ -671,7 +622,6 @@ pub struct GetUserAgentResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct UserInfoResponse {
     /// Note: `alleged_user_email` is not currently verified. We read it from
@@ -688,15 +638,12 @@ pub struct GetUserSavedConfigResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct SetDefaultModelParams {
     /// If set to None, this means `model` should be cleared in config.toml.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model: Option<String>,
     /// If set to None, this means `model_reasoning_effort` should be cleared
     /// in config.toml.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reasoning_effort: Option<ReasoningEffort>,
 }
 
@@ -708,46 +655,32 @@ pub struct SetDefaultModelResponse {}
 /// client-configurable settings that can be specified in the NewConversation
 /// and SendUserTurn requests.
 #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct UserSavedConfig {
     /// Approvals
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub approval_policy: Option<AskForApproval>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub sandbox_mode: Option<SandboxMode>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub sandbox_settings: Option<SandboxSettings>,
 
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub forced_chatgpt_workspace_id: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub forced_login_method: Option<ForcedLoginMethod>,
 
     /// Model-specific configuration
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model_reasoning_effort: Option<ReasoningEffort>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model_reasoning_summary: Option<ReasoningSummary>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub model_verbosity: Option<Verbosity>,
 
     /// Tools
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub tools: Option<Tools>,
 
     /// Profiles
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub profile: Option<String>,
-    #[serde(default)]
     pub profiles: HashMap<String, Profile>,
 }
 
 /// MCP representation of a [`codex_core::config_profile::ConfigProfile`].
 #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct Profile {
     pub model: Option<String>,
@@ -760,29 +693,23 @@ pub struct Profile {
     pub model_verbosity: Option<Verbosity>,
     pub chatgpt_base_url: Option<String>,
 }
+
 /// MCP representation of a [`codex_core::config::ToolsToml`].
 #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct Tools {
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub web_search: Option<bool>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub view_image: Option<bool>,
 }
 
 /// MCP representation of a [`codex_core::config::types::SandboxWorkspaceWrite`].
 #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct SandboxSettings {
     #[serde(default)]
     pub writable_roots: Vec<PathBuf>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub network_access: Option<bool>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub exclude_tmpdir_env_var: Option<bool>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub exclude_slash_tmp: Option<bool>,
 }
 
@@ -794,7 +721,6 @@ pub struct SendUserMessageParams {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct SendUserTurnParams {
     pub conversation_id: ConversationId,
@@ -803,7 +729,6 @@ pub struct SendUserTurnParams {
     pub approval_policy: AskForApproval,
     pub sandbox_policy: SandboxPolicy,
     pub model: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub effort: Option<ReasoningEffort>,
     pub summary: ReasoningSummary,
 }
@@ -938,7 +863,6 @@ server_request_definitions! {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ApplyPatchApprovalParams {
     pub conversation_id: ConversationId,
@@ -947,16 +871,13 @@ pub struct ApplyPatchApprovalParams {
     pub call_id: String,
     pub file_changes: HashMap<PathBuf, FileChange>,
     /// Optional explanatory reason (e.g. request for extra write access).
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reason: Option<String>,
     /// When set, the agent is asking the user to allow writes under this root
     /// for the remainder of the session (unclear if this is honored today).
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub grant_root: Option<PathBuf>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct ExecCommandApprovalParams {
     pub conversation_id: ConversationId,
@@ -965,9 +886,7 @@ pub struct ExecCommandApprovalParams {
     pub call_id: String,
     pub command: Vec<String>,
     pub cwd: PathBuf,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reason: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub risk: Option<SandboxCommandAssessment>,
     pub parsed_cmd: Vec<ParsedCommand>,
 }
@@ -983,26 +902,22 @@ pub struct ApplyPatchApprovalResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 #[ts(rename_all = "camelCase")]
 pub struct FuzzyFileSearchParams {
     pub query: String,
     pub roots: Vec<String>,
     // if provided, will cancel any previous request that used the same value
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub cancellation_token: Option<String>,
 }
 
 /// Superset of [`codex_file_search::FileMatch`]
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 pub struct FuzzyFileSearchResult {
     pub root: String,
     pub path: String,
     pub file_name: String,
     pub score: u32,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub indices: Option<Vec<u32>>,
 }
 
@@ -1012,18 +927,15 @@ pub struct FuzzyFileSearchResponse {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct LoginChatGptCompleteNotification {
     #[schemars(with = "String")]
     pub login_id: Uuid,
     pub success: bool,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub error: Option<String>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct SessionConfiguredNotification {
     /// Name left as session_id instead of conversation_id for backwards compatibility.
@@ -1033,7 +945,6 @@ pub struct SessionConfiguredNotification {
     pub model: String,
 
     /// The effort the model is putting into reasoning about the user's request.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub reasoning_effort: Option<ReasoningEffort>,
 
     /// Identifier of the history log file (inode on Unix, 0 otherwise).
@@ -1045,18 +956,15 @@ pub struct SessionConfiguredNotification {
 
     /// Optional initial messages (as events) for resumed sessions.
     /// When present, UIs can use these to seed the history.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub initial_messages: Option<Vec<EventMsg>>,
 
     pub rollout_path: PathBuf,
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 #[serde(rename_all = "camelCase")]
 pub struct AuthStatusChangeNotification {
     /// Current authentication method; omitted if signed out.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub auth_method: Option<AuthMode>,
 }
 
@@ -1139,7 +1047,14 @@ mod tests {
                 "id": 42,
                 "params": {
                     "model": "gpt-5-codex",
-                    "approvalPolicy": "on-request"
+                    "modelProvider": null,
+                    "profile": null,
+                    "cwd": null,
+                    "approvalPolicy": "on-request",
+                    "sandbox": null,
+                    "config": null,
+                    "baseInstructions": null,
+                    "includeApplyPatchTool": null
                 }
             }),
             serde_json::to_value(&request)?,
@@ -1212,6 +1127,7 @@ mod tests {
                     "command": ["echo", "hello"],
                     "cwd": "/tmp",
                     "reason": "because tests",
+                    "risk": null,
                     "parsedCmd": [
                         {
                             "type": "unknown",
@@ -1356,7 +1272,10 @@ mod tests {
             json!({
                 "method": "model/list",
                 "id": 6,
-                "params": {}
+                "params": {
+                    "pageSize": null,
+                    "cursor": null
+                }
             }),
             serde_json::to_value(&request)?,
         );
diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs
index feaa19e37a88..3f4497fb237e 100644
--- a/codex-rs/app-server/src/outgoing_message.rs
+++ b/codex-rs/app-server/src/outgoing_message.rs
@@ -166,6 +166,7 @@ mod tests {
                 "params": {
                     "loginId": Uuid::nil(),
                     "success": true,
+                    "error": null,
                 },
             }),
             serde_json::to_value(jsonrpc_notification)
diff --git a/codex-rs/mcp-types/generate_mcp_types.py b/codex-rs/mcp-types/generate_mcp_types.py
index 19060335370e..7535b4c760e0 100755
--- a/codex-rs/mcp-types/generate_mcp_types.py
+++ b/codex-rs/mcp-types/generate_mcp_types.py
@@ -332,6 +332,7 @@ class StructField:
     name: str
     type_name: str
     serde: str | None = None
+    ts: str | None = None
     comment: str | None = None
 
     def append(self, out: list[str], supports_const: bool) -> None:
@@ -339,6 +340,8 @@ def append(self, out: list[str], supports_const: bool) -> None:
             out.append(f"    // {self.comment}\n")
         if self.serde:
             out.append(f"    {self.serde}\n")
+        if self.ts:
+            out.append(f"    {self.ts}\n")
         if self.viz == "const":
             if supports_const:
                 out.append(f"    const {self.name}: {self.type_name};\n")
@@ -378,9 +381,9 @@ def define_struct(
             prop_type = f"Option<{prop_type}>"
         rs_prop = rust_prop_name(prop_name, is_optional)
         if prop_type.startswith("&'static str"):
-            fields.append(StructField("const", rs_prop.name, prop_type, rs_prop.serde))
+            fields.append(StructField("const", rs_prop.name, prop_type, rs_prop.serde, rs_prop.ts))
         else:
-            fields.append(StructField("pub", rs_prop.name, prop_type, rs_prop.serde))
+            fields.append(StructField("pub", rs_prop.name, prop_type, rs_prop.serde, rs_prop.ts))
 
     # Special-case: add Codex-specific user_agent to Implementation
     if name == "Implementation":
@@ -390,6 +393,7 @@ def define_struct(
                 "user_agent",
                 "Option<String>",
                 '#[serde(default, skip_serializing_if = "Option::is_none")]',
+                '#[ts(optional)]',
                 "This is an extra field that the Codex MCP server sends as part of InitializeResult.",
             )
         )
@@ -474,7 +478,6 @@ def define_string_enum(
         out.append(f"    {capitalize(value)},\n")
 
     out.append("}\n\n")
-    return out
 
 
 def define_untagged_enum(name: str, type_list: list[str], out: list[str]) -> None:
@@ -590,7 +593,7 @@ def get_serde_annotation_for_anyof_type(type_name: str) -> str | None:
 
 
 def map_type(
-    typedef: dict[str, any],
+    typedef: dict[str, Any],
     prop_name: str | None = None,
     struct_name: str | None = None,
 ) -> str:
@@ -665,7 +668,8 @@ class RustProp:
     name: str
     # serde annotation, if necessary
     serde: str | None = None
-
+    # ts annotation, if necessary
+    ts: str | None = None
 
 def rust_prop_name(name: str, is_optional: bool) -> RustProp:
     """Convert a JSON property name to a Rust property name."""
@@ -684,6 +688,7 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp:
         prop_name = name
 
     serde_annotations = []
+    ts_str = None
     if is_rename:
         serde_annotations.append(f'rename = "{name}"')
     if is_optional:
@@ -691,13 +696,18 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp:
         serde_annotations.append('skip_serializing_if = "Option::is_none"')
 
     if serde_annotations:
+        # Also mark optional fields for ts-rs generation.
         serde_str = f"#[serde({', '.join(serde_annotations)})]"
     else:
         serde_str = None
-    return RustProp(prop_name, serde_str)
+
+    if is_optional and serde_str:
+        ts_str = "#[ts(optional)]"
+
+    return RustProp(prop_name, serde_str, ts_str)
 
 
-def to_snake_case(name: str) -> str:
+def to_snake_case(name: str) -> str | None:
     """Convert a camelCase or PascalCase name to snake_case."""
     snake_case = name[0].lower() + "".join("_" + c.lower() if c.isupper() else c for c in name[1:])
     if snake_case != name:
diff --git a/codex-rs/mcp-types/src/lib.rs b/codex-rs/mcp-types/src/lib.rs
index 98dab7252b3b..e390322fc811 100644
--- a/codex-rs/mcp-types/src/lib.rs
+++ b/codex-rs/mcp-types/src/lib.rs
@@ -37,14 +37,17 @@ fn default_jsonrpc() -> String {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct Annotations {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub audience: Option<Vec<Role>>,
     #[serde(
         rename = "lastModified",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub last_modified: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub priority: Option<f64>,
 }
 
@@ -52,6 +55,7 @@ pub struct Annotations {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct AudioContent {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     pub data: String,
     #[serde(rename = "mimeType")]
@@ -64,6 +68,7 @@ pub struct AudioContent {
 pub struct BaseMetadata {
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
 }
 
@@ -71,6 +76,7 @@ pub struct BaseMetadata {
 pub struct BlobResourceContents {
     pub blob: String,
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub uri: String,
 }
@@ -78,10 +84,13 @@ pub struct BlobResourceContents {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct BooleanSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub default: Option<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String, // &'static str = "boolean"
 }
@@ -98,6 +107,7 @@ impl ModelContextProtocolRequest for CallToolRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct CallToolRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub arguments: Option<serde_json::Value>,
     pub name: String,
 }
@@ -107,12 +117,14 @@ pub struct CallToolRequestParams {
 pub struct CallToolResult {
     pub content: Vec<ContentBlock>,
     #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub is_error: Option<bool>,
     #[serde(
         rename = "structuredContent",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub structured_content: Option<serde_json::Value>,
 }
 
@@ -135,6 +147,7 @@ impl ModelContextProtocolNotification for CancelledNotification {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct CancelledNotificationParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub reason: Option<String>,
     #[serde(rename = "requestId")]
     pub request_id: RequestId,
@@ -144,12 +157,16 @@ pub struct CancelledNotificationParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ClientCapabilities {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub elicitation: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub experimental: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub roots: Option<ClientCapabilitiesRoots>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub sampling: Option<serde_json::Value>,
 }
 
@@ -161,6 +178,7 @@ pub struct ClientCapabilitiesRoots {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub list_changed: Option<bool>,
 }
 
@@ -228,6 +246,7 @@ impl ModelContextProtocolRequest for CompleteRequest {
 pub struct CompleteRequestParams {
     pub argument: CompleteRequestParamsArgument,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub context: Option<CompleteRequestParamsContext>,
     pub r#ref: CompleteRequestParamsRef,
 }
@@ -236,6 +255,7 @@ pub struct CompleteRequestParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct CompleteRequestParamsContext {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub arguments: Option<serde_json::Value>,
 }
 
@@ -262,8 +282,10 @@ pub struct CompleteResult {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct CompleteResultCompletion {
     #[serde(rename = "hasMore", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub has_more: Option<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub total: Option<i64>,
     pub values: Vec<String>,
 }
@@ -302,31 +324,37 @@ pub struct CreateMessageRequestParams {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub include_context: Option<String>,
     #[serde(rename = "maxTokens")]
     pub max_tokens: i64,
     pub messages: Vec<SamplingMessage>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub metadata: Option<serde_json::Value>,
     #[serde(
         rename = "modelPreferences",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub model_preferences: Option<ModelPreferences>,
     #[serde(
         rename = "stopSequences",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub stop_sequences: Option<Vec<String>>,
     #[serde(
         rename = "systemPrompt",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub system_prompt: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub temperature: Option<f64>,
 }
 
@@ -341,6 +369,7 @@ pub struct CreateMessageResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub stop_reason: Option<String>,
 }
 
@@ -385,6 +414,7 @@ pub struct ElicitRequestParams {
 pub struct ElicitRequestParamsRequestedSchema {
     pub properties: serde_json::Value,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub required: Option<Vec<String>>,
     pub r#type: String, // &'static str = "object"
 }
@@ -394,6 +424,7 @@ pub struct ElicitRequestParamsRequestedSchema {
 pub struct ElicitResult {
     pub action: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub content: Option<serde_json::Value>,
 }
 
@@ -412,6 +443,7 @@ impl From<ElicitResult> for serde_json::Value {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct EmbeddedResource {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     pub resource: EmbeddedResourceResource,
     pub r#type: String, // &'static str = "resource"
@@ -429,11 +461,14 @@ pub type EmptyResult = Result;
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct EnumSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     pub r#enum: Vec<String>,
     #[serde(rename = "enumNames", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub enum_names: Option<Vec<String>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String, // &'static str = "string"
 }
@@ -450,6 +485,7 @@ impl ModelContextProtocolRequest for GetPromptRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct GetPromptRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub arguments: Option<serde_json::Value>,
     pub name: String,
 }
@@ -458,6 +494,7 @@ pub struct GetPromptRequestParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct GetPromptResult {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     pub messages: Vec<PromptMessage>,
 }
@@ -474,6 +511,7 @@ impl From<GetPromptResult> for serde_json::Value {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ImageContent {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     pub data: String,
     #[serde(rename = "mimeType")]
@@ -486,10 +524,12 @@ pub struct ImageContent {
 pub struct Implementation {
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub version: String,
     // This is an extra field that the Codex MCP server sends as part of InitializeResult.
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub user_agent: Option<String>,
 }
 
@@ -516,6 +556,7 @@ pub struct InitializeRequestParams {
 pub struct InitializeResult {
     pub capabilities: ServerCapabilities,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub instructions: Option<String>,
     #[serde(rename = "protocolVersion")]
     pub protocol_version: String,
@@ -552,6 +593,7 @@ pub struct JSONRPCError {
 pub struct JSONRPCErrorError {
     pub code: i64,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub data: Option<serde_json::Value>,
     pub message: String,
 }
@@ -573,6 +615,7 @@ pub struct JSONRPCNotification {
     pub jsonrpc: String,
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
@@ -584,6 +627,7 @@ pub struct JSONRPCRequest {
     pub jsonrpc: String,
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
@@ -608,6 +652,7 @@ impl ModelContextProtocolRequest for ListPromptsRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ListPromptsRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub cursor: Option<String>,
 }
 
@@ -619,6 +664,7 @@ pub struct ListPromptsResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub next_cursor: Option<String>,
     pub prompts: Vec<Prompt>,
 }
@@ -643,6 +689,7 @@ impl ModelContextProtocolRequest for ListResourceTemplatesRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ListResourceTemplatesRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub cursor: Option<String>,
 }
 
@@ -654,6 +701,7 @@ pub struct ListResourceTemplatesResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub next_cursor: Option<String>,
     #[serde(rename = "resourceTemplates")]
     pub resource_templates: Vec<ResourceTemplate>,
@@ -679,6 +727,7 @@ impl ModelContextProtocolRequest for ListResourcesRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ListResourcesRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub cursor: Option<String>,
 }
 
@@ -690,6 +739,7 @@ pub struct ListResourcesResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub next_cursor: Option<String>,
     pub resources: Vec<Resource>,
 }
@@ -739,6 +789,7 @@ impl ModelContextProtocolRequest for ListToolsRequest {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ListToolsRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub cursor: Option<String>,
 }
 
@@ -750,6 +801,7 @@ pub struct ListToolsResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub next_cursor: Option<String>,
     pub tools: Vec<Tool>,
 }
@@ -799,6 +851,7 @@ pub struct LoggingMessageNotificationParams {
     pub data: serde_json::Value,
     pub level: LoggingLevel,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub logger: Option<String>,
 }
 
@@ -809,6 +862,7 @@ pub struct LoggingMessageNotificationParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ModelHint {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub name: Option<String>,
 }
 
@@ -830,20 +884,24 @@ pub struct ModelPreferences {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub cost_priority: Option<f64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub hints: Option<Vec<ModelHint>>,
     #[serde(
         rename = "intelligencePriority",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub intelligence_priority: Option<f64>,
     #[serde(
         rename = "speedPriority",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub speed_priority: Option<f64>,
 }
 
@@ -851,18 +909,23 @@ pub struct ModelPreferences {
 pub struct Notification {
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct NumberSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub maximum: Option<i64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub minimum: Option<i64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String,
 }
@@ -871,12 +934,14 @@ pub struct NumberSchema {
 pub struct PaginatedRequest {
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<PaginatedRequestParams>,
 }
 
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct PaginatedRequestParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub cursor: Option<String>,
 }
 
@@ -887,6 +952,7 @@ pub struct PaginatedResult {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub next_cursor: Option<String>,
 }
 
@@ -929,11 +995,13 @@ impl ModelContextProtocolNotification for ProgressNotification {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ProgressNotificationParams {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub message: Option<String>,
     pub progress: f64,
     #[serde(rename = "progressToken")]
     pub progress_token: ProgressToken,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub total: Option<f64>,
 }
 
@@ -948,11 +1016,14 @@ pub enum ProgressToken {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct Prompt {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub arguments: Option<Vec<PromptArgument>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
 }
 
@@ -960,11 +1031,14 @@ pub struct Prompt {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct PromptArgument {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub required: Option<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
 }
 
@@ -991,6 +1065,7 @@ pub struct PromptMessage {
 pub struct PromptReference {
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String, // &'static str = "ref/prompt"
 }
@@ -1034,6 +1109,7 @@ impl From<ReadResourceResult> for serde_json::Value {
 pub struct Request {
     pub method: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub params: Option<serde_json::Value>,
 }
 
@@ -1048,15 +1124,20 @@ pub enum RequestId {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct Resource {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub size: Option<i64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub uri: String,
 }
@@ -1065,6 +1146,7 @@ pub struct Resource {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ResourceContents {
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub uri: String,
 }
@@ -1075,15 +1157,20 @@ pub struct ResourceContents {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ResourceLink {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub size: Option<i64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String, // &'static str = "resource_link"
     pub uri: String,
@@ -1101,13 +1188,17 @@ impl ModelContextProtocolNotification for ResourceListChangedNotification {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ResourceTemplate {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub name: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     #[serde(rename = "uriTemplate")]
     pub uri_template: String,
@@ -1148,6 +1239,7 @@ pub enum Role {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct Root {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub name: Option<String>,
     pub uri: String,
 }
@@ -1179,16 +1271,22 @@ pub enum SamplingMessageContent {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ServerCapabilities {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub completions: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub experimental: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub logging: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub prompts: Option<ServerCapabilitiesPrompts>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub resources: Option<ServerCapabilitiesResources>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub tools: Option<ServerCapabilitiesTools>,
 }
 
@@ -1200,6 +1298,7 @@ pub struct ServerCapabilitiesTools {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub list_changed: Option<bool>,
 }
 
@@ -1211,8 +1310,10 @@ pub struct ServerCapabilitiesResources {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub list_changed: Option<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub subscribe: Option<bool>,
 }
 
@@ -1224,6 +1325,7 @@ pub struct ServerCapabilitiesPrompts {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub list_changed: Option<bool>,
 }
 
@@ -1298,14 +1400,19 @@ pub struct SetLevelRequestParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct StringSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub format: Option<String>,
     #[serde(rename = "maxLength", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub max_length: Option<i64>,
     #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub min_length: Option<i64>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
     pub r#type: String, // &'static str = "string"
 }
@@ -1328,6 +1435,7 @@ pub struct SubscribeRequestParams {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct TextContent {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<Annotations>,
     pub text: String,
     pub r#type: String, // &'static str = "text"
@@ -1336,6 +1444,7 @@ pub struct TextContent {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct TextResourceContents {
     #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub mime_type: Option<String>,
     pub text: String,
     pub uri: String,
@@ -1345,8 +1454,10 @@ pub struct TextResourceContents {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct Tool {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub annotations: Option<ToolAnnotations>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub description: Option<String>,
     #[serde(rename = "inputSchema")]
     pub input_schema: ToolInputSchema,
@@ -1356,8 +1467,10 @@ pub struct Tool {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub output_schema: Option<ToolOutputSchema>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
 }
 
@@ -1366,8 +1479,10 @@ pub struct Tool {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ToolOutputSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub properties: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub required: Option<Vec<String>>,
     pub r#type: String, // &'static str = "object"
 }
@@ -1376,8 +1491,10 @@ pub struct ToolOutputSchema {
 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
 pub struct ToolInputSchema {
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub properties: Option<serde_json::Value>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub required: Option<Vec<String>>,
     pub r#type: String, // &'static str = "object"
 }
@@ -1397,26 +1514,31 @@ pub struct ToolAnnotations {
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub destructive_hint: Option<bool>,
     #[serde(
         rename = "idempotentHint",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub idempotent_hint: Option<bool>,
     #[serde(
         rename = "openWorldHint",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub open_world_hint: Option<bool>,
     #[serde(
         rename = "readOnlyHint",
         default,
         skip_serializing_if = "Option::is_none"
     )]
+    #[ts(optional)]
     pub read_only_hint: Option<bool>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
     pub title: Option<String>,
 }
 
diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs
index 77a1b50fec77..d608dba639ed 100644
--- a/codex-rs/protocol/src/approvals.rs
+++ b/codex-rs/protocol/src/approvals.rs
@@ -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,
diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs
index 790e0df82123..7472d1b4c5c4 100644
--- a/codex-rs/protocol/src/custom_prompts.rs
+++ b/codex-rs/protocol/src/custom_prompts.rs
@@ -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,
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index f7b31c4d6bf4..a824ee91e4cc 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -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")]
diff --git a/codex-rs/protocol/src/parse_command.rs b/codex-rs/protocol/src/parse_command.rs
index b74713b45220..77926f11b66f 100644
--- a/codex-rs/protocol/src/parse_command.rs
+++ b/codex-rs/protocol/src/parse_command.rs
@@ -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 {
diff --git a/codex-rs/protocol/src/plan_tool.rs b/codex-rs/protocol/src/plan_tool.rs
index a95fe32efd48..a9038eb03ba3 100644
--- a/codex-rs/protocol/src/plan_tool.rs
+++ b/codex-rs/protocol/src/plan_tool.rs
@@ -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>,
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index b16baab9b038..dcb0299e492b 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -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>,
     },
 }
diff --git a/codex-rs/utils/git/src/lib.rs b/codex-rs/utils/git/src/lib.rs
index 57ab05b38bb7..cf2887aa8f7b 100644
--- a/codex-rs/utils/git/src/lib.rs
+++ b/codex-rs/utils/git/src/lib.rs
@@ -28,7 +28,6 @@ type CommitID = String;
 
 /// Details of a ghost commit created from a repository state.
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
-#[ts(optional_fields = nullable)]
 pub struct GhostCommit {
     id: CommitID,
     parent: Option<CommitID>,
