Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 1cbd7d8

Browse files
fix: don't insert Null for Option fields in default filling
When Option<T> fields are flattened inside enum variants, inserting ConfigValue::Null causes the facet deserializer to produce Some(Default::default()) instead of None (e.g., Some("") for String, Some(0) for usize). Fix: return None from get_default_config_value for Option types, leaving the field missing so facet applies the natural Default for Option<T>, which is None. Works correctly in both flatten and non-flatten code paths.
1 parent 9d29e1a commit 1cbd7d8

1 file changed

Lines changed: 59 additions & 7 deletions

File tree

crates/figue/src/config_value_parser.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -945,14 +945,13 @@ fn get_default_config_value(
945945
return None;
946946
}
947947

948-
// Option<T> implicitly has Default semantics (None)
949-
// Check type_identifier for "Option" - handles both std::option::Option and core::option::Option
948+
// Option<T> implicitly has Default semantics (None).
949+
// We don't insert ConfigValue::Null here because the facet deserializer
950+
// may mishandle Null for flattened fields inside enum variants.
951+
// Instead, leave the field missing and let facet apply the natural
952+
// Default for Option<T>, which is None.
950953
if shape.type_identifier.contains("Option") {
951-
return Some(ConfigValue::Null(Sourced {
952-
value: (),
953-
span: None,
954-
provenance: Some(Provenance::Default),
955-
}));
954+
return None;
956955
}
957956

958957
// For struct types without explicit defaults, create empty object for recursive filling
@@ -2423,4 +2422,57 @@ mod fill_defaults_tests {
24232422
panic!("expected enum");
24242423
}
24252424
}
2425+
2426+
// Test 8: Enum with flatten containing Option<String> fields
2427+
#[derive(Facet, Debug)]
2428+
struct TranscribeArgs {
2429+
config: Option<String>,
2430+
language: Option<String>,
2431+
pulse_limit: Option<usize>,
2432+
}
2433+
2434+
#[derive(Facet, Debug)]
2435+
#[repr(u8)]
2436+
#[allow(dead_code)]
2437+
enum EnumWithFlattenOption {
2438+
Run {
2439+
#[facet(flatten)]
2440+
args: TranscribeArgs,
2441+
},
2442+
}
2443+
2444+
#[test]
2445+
fn test_fill_defaults_enum_flatten_option_fields() {
2446+
let fields = IndexMap::default();
2447+
let input = ConfigValue::Enum(Sourced::new(crate::config_value::EnumValue {
2448+
variant: "Run".to_string(),
2449+
fields,
2450+
}));
2451+
let result = fill_defaults_from_shape(&input, EnumWithFlattenOption::SHAPE);
2452+
2453+
if let ConfigValue::Enum(e) = &result {
2454+
println!("result fields: {:#?}", e.value.fields);
2455+
2456+
// Option fields should NOT be filled - they stay missing
2457+
// and the facet deserializer provides None by default.
2458+
assert!(!e.value.fields.contains_key("config"));
2459+
assert!(!e.value.fields.contains_key("language"));
2460+
assert!(!e.value.fields.contains_key("pulse_limit"));
2461+
} else {
2462+
panic!("expected enum, got {:?}", result);
2463+
}
2464+
2465+
// Now try full deserialization
2466+
let deserialized: EnumWithFlattenOption =
2467+
from_config_value(&input).expect("deserialization should succeed");
2468+
println!("deserialized: {:?}", deserialized);
2469+
2470+
if let EnumWithFlattenOption::Run { args } = &deserialized {
2471+
assert_eq!(args.config, None);
2472+
assert_eq!(args.language, None);
2473+
assert_eq!(args.pulse_limit, None);
2474+
} else {
2475+
panic!("expected Run variant");
2476+
}
2477+
}
24262478
}

0 commit comments

Comments
 (0)