spotify / spotify/confidence-sdk-rust

Null variant property resolves to type zero-value instead of the call-site default

Open
#33 0 comments 0 reactions 0 assignees View on GitHub

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 — a null variant property means "no value defined", so the SDK falls back to the call-site default.
  • Actual: enabled == false — the Rust zero value for bool, regardless of the call-site default.

Same for i64 (→ 0), f64 (→ 0.0), and String (→ "").

Root cause

Two spots:

  1. confidence/src/confidence_value.rs — the ConfidenceValue enum has no Null variant (Bool/Int/Float/String/Array/Struct only), so a backend null can't be represented as a value.

  2. confidence/src/models.rsinto_value coerces 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_flagas_typeTypeConversionTrait::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 nullget_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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.