spotify / spotify/confidence-sdk-rust
Null variant property resolves to type zero-value instead of the call-site default
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 2
- Forks
- 3
- Avg merge
- 20h 15m
- Merged PRs (30d)
- 3
Description
Summary
When a resolved flag has a property whose value is null, confidence-sdk-rust serves the type's zero value (false / 0 / 0.0 / "") instead of the call-site default value passed to get_flag / get_bool_value / etc.
Every other Confidence SDK I checked — JS, Swift, Android/Kotlin, Python, Java, Go — returns the call-site default for a null property, as do all six local-resolve providers in spotify/confidence-resolver (go, java, js, python, ruby, and the Rust local provider). The standalone confidence-sdk-rust is the only implementation that diverges.
Expected vs actual
Given a flag whose served variant has {"enabled": null} and code:
let enabled = client.get_bool_value(ctx, "my-flag.enabled", true, eval_ctx).await; // call-site default = true
- Expected (matches all other SDKs):
enabled == true— anullvariant property means "no value defined", so the SDK falls back to the call-site default. - Actual:
enabled == false— the Rust zero value forbool, regardless of the call-site default.
Same for i64 (→ 0), f64 (→ 0.0), and String (→ "").
Root cause
Two spots:
-
confidence/src/confidence_value.rs— theConfidenceValueenum has noNullvariant (Bool/Int/Float/String/Array/Structonly), so a backendnullcan't be represented as a value. -
confidence/src/models.rs—into_valuecoerces each field by its schema type:
SchemaType::BoolType => ConfidenceValue::Bool(value.as_bool().unwrap_or_default()),
SchemaType::IntType => ConfidenceValue::Int(value.as_i64().unwrap_or_default()),
SchemaType::DoubleType => ConfidenceValue::Float(value.as_f64().unwrap_or_default()),
SchemaType::StringType => ConfidenceValue::String(value.as_str().unwrap_or_default().to_string()),
For a JSON null, serde_json's as_bool() / as_i64() / as_f64() / as_str() return None, so unwrap_or_default() produces the type's zero value. That concrete zero is stored as e.g. ConfidenceValue::Bool(false), then process_flag (in lib.rs) finds it and returns it with EvaluationReason::TargetingMatch, and get_flag → as_type → TypeConversionTrait::process returns it. The call-site default is never consulted.
For reference, the documented intent is explicit in the Swift SDK:
// `null` type from backend instructs to use client-side default value
Proposed fix (minimal)
Treat a null field as absent during conversion: in models.rs into_value, skip inserting a field whose serde_json::Value is Null. Then process_flag's last_struct.fields.get(path) returns None for that property → the resolution returns the not-found path, and the OpenFeature layer returns the user's default — which is exactly how a missing property is already handled, and how the other SDKs behave.
A regression test should cover: a variant property explicitly set to null → get_bool_value (and the int/float/string variants) returns the call-site default, not the zero value.
(An alternative, more faithful to the "null = use client-side default" semantic, is to add a ConfidenceValue::Null variant and have as_type/process return the default for it — but that touches the enum and every exhaustive match, so the skip-on-null approach above is the smaller, lower-risk change.)
How this was found
While building automated stale-flag cleanup tooling that inlines a single-variant flag's served values into call sites, I needed the exact null-property semantics and audited the value-resolution code across all Confidence SDKs and the local-resolve providers. 12 of 13 implementations return the call-site default for a null property; confidence-sdk-rust is the lone exception.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in confidence/src/confidence_value.rs and confidence/src/models.rs, then trace process_flag in lib.rs and the get_flag/as_type conversion path. Add regression coverage for null bool, integer, float, and string properties, verifying each returns its call-site default rather than the type zero value.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100