From 08bd7727fff24029fc4ec885742934e816c1b37a Mon Sep 17 00:00:00 2001 From: Max Makarov Date: Wed, 3 Jun 2026 04:30:09 +0000 Subject: [PATCH] vmm: omit unset Option fields from API responses VmConfig and its nested configuration structs, the VmInfoResponse wrapper and DeviceNode serialize their Option fields as JSON null when unset. The OpenAPI specification types these fields as non-nullable, so strict client generators (for example ogen for Go) reject /vm.info responses and cannot generate a working API client. Apply serde_with's skip_serializing_none to the affected structs so that unset optional fields are omitted from the serialized JSON instead of being emitted as null. API responses now validate against the existing specification unchanged; no nullable annotations are required. Fixes: #7775 Signed-off-by: Max Makarov Assisted-by: Claude:claude-opus-4-8 [Claude Code] --- Cargo.lock | 1 + vmm/Cargo.toml | 1 + vmm/src/api/mod.rs | 1 + vmm/src/device_tree.rs | 1 + vmm/src/vm_config.rs | 16 ++++++++++++++++ 5 files changed, 20 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e7d6040af..83f1fefe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2737,6 +2737,7 @@ dependencies = [ "seccompiler", "serde", "serde_json", + "serde_with", "serial_buffer", "sha2", "signal-hook", diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index f3de03cb5..fe5901b0e 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -80,6 +80,7 @@ rate_limiter = { path = "../rate_limiter" } seccompiler = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } +serde_with = { workspace = true, features = ["macros"] } serial_buffer = { path = "../serial_buffer" } sha2 = { workspace = true } signal-hook = { workspace = true } diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs index e4ee7235a..8e1c2b406 100644 --- a/vmm/src/api/mod.rs +++ b/vmm/src/api/mod.rs @@ -213,6 +213,7 @@ pub enum ApiError { } pub type ApiResult = Result; +#[serde_with::skip_serializing_none] #[derive(Clone, Deserialize, Serialize)] pub struct VmInfoResponse { pub config: Box, diff --git a/vmm/src/device_tree.rs b/vmm/src/device_tree.rs index c1cb000df..6116cd55b 100644 --- a/vmm/src/device_tree.rs +++ b/vmm/src/device_tree.rs @@ -12,6 +12,7 @@ use vm_migration::Migratable; use crate::device_manager::PciDeviceHandle; +#[serde_with::skip_serializing_none] #[derive(Clone, Serialize, Deserialize)] pub struct DeviceNode { pub id: String, diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 16351d3de..02efad065 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -67,6 +67,7 @@ pub fn default_cpuconfig_max_phys_bits() -> u8 { DEFAULT_MAX_PHYS_BITS } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CpusConfig { pub boot_vcpus: u32, @@ -123,6 +124,7 @@ pub fn default_platformconfig_vfio_p2p_dma() -> bool { true } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct PlatformConfig { #[serde(default = "default_platformconfig_num_pci_segments")] @@ -221,6 +223,7 @@ pub struct PciSegmentConfig { pub mmio64_aperture_weight: u32, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct MemoryZoneConfig { pub id: String, @@ -269,6 +272,7 @@ fn default_cpusconfig_nested() -> bool { true } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct MemoryConfig { pub size: u64, @@ -335,6 +339,7 @@ pub struct VirtQueueAffinity { pub host_cpus: Box<[usize]>, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct PciDeviceCommonConfig { #[serde(default)] @@ -347,6 +352,7 @@ pub struct PciDeviceCommonConfig { pub pci_device_id: Option, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DiskConfig { #[serde(flatten)] @@ -412,6 +418,7 @@ pub fn default_diskconfig_sparse() -> bool { true } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct NetConfig { #[serde(flatten)] @@ -584,6 +591,7 @@ impl ApplyLandlock for GenericVhostUserConfig { } } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct PmemConfig { #[serde(flatten)] @@ -616,6 +624,7 @@ pub enum ConsoleOutputMode { /// Common configuration for plain console configs. /// /// Independent of PCI or legacy devices. +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CommonConsoleConfig { #[serde(default)] @@ -703,6 +712,7 @@ impl ApplyLandlock for ConsoleConfig { } #[cfg(target_arch = "x86_64")] +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DebugConsoleConfig { #[serde(default)] @@ -736,6 +746,7 @@ impl ApplyLandlock for DebugConsoleConfig { } } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DeviceConfig { #[serde(flatten)] @@ -846,6 +857,7 @@ pub struct NumaDistance { pub distance: u8, } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct NumaConfig { pub guest_numa_id: u32, @@ -898,6 +910,7 @@ pub enum PayloadConfigError { FwCfgInvalidItem(String), } +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct PayloadConfig { #[serde(default)] @@ -919,6 +932,7 @@ pub struct PayloadConfig { } #[cfg(feature = "fw_cfg")] +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct FwCfgConfig { pub e820: bool, @@ -951,6 +965,7 @@ pub struct FwCfgItemList { } #[cfg(feature = "fw_cfg")] +#[serde_with::skip_serializing_none] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct FwCfgItem { #[serde(default)] @@ -1081,6 +1096,7 @@ impl ApplyLandlock for LandlockConfig { } } +#[serde_with::skip_serializing_none] #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct VmConfig { #[serde(default)]