Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[bug] Any JSON: non-string "value" for unregistered type URL decodes to empty

Closed Beginner friendly
#487 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 3 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
88/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust
Domain
api, backend

Research direction

Start in buffa-types/src/any_ext.rs at the unregistered-type-URL fallback around lines 398-404, then read fallback_base64_without_registry and fallback_base64_with_registry_but_unknown_type around lines 858 and 895. Add regression coverage for non-string value shapes while keeping valid base64 behavior covered, and run the focused Any deserialization tests to confirm malformed inputs error without changing valid fallback decoding.

Written by the indexing model from the issue text.

Description

Summary

Any's JSON deserializer silently accepts a value of any JSON type on the unregistered-type-URL path and decodes it to an empty payload, so a malformed document becomes a successfully parsed Any with zero bytes, which then unpacks into an all-default message.

Reproduction

// features: json (serde_json), no AnyRegistry entries
use buffa_types::Any;

let a: Any = serde_json::from_str(
    r#"{"@type":"type.example.com/Unknown","value":{"a":1}}"#
).unwrap();
assert_eq!(a.value, Vec::new()); // passes: the object was discarded

value can also be a number, an array, a bool, or null; all five come back as Ok(Any { type_url, value: [] }).

In buffa-types/src/any_ext.rs, the None => match obj.remove("value") arm (around :398) has three cases:

  • Some(serde_json::Value::String(s)) -> base64 decode, :400-403
  • _ => alloc::vec::Vec::new() -> no error, :404

Why this looks unintended

The doc comment on the impl states this path is base64 (any_ext.rs:280-283: "Falls back to base64-encoded value when the registry is absent or the type URL is not registered"), and the serializer it inverts only ever produces a string (:344-349, map.serialize_entry("value", &Base64Bytes(&self.value))). The comment above the branch says what it is for: "Fallback: base64 decode the "value" field." (any_ext.rs:399). Decoding something that is not a string is not a base64 decode that happened to produce zero bytes — it is input the branch has no defined behaviour for.

The same deserialize rejects every other shape of the same mistake, which is why this looks unintended:

bad input result where
"@type": 5 Err("@type must be a string") :362-365
"@type": "NoSlash" Err("@type must be a valid type URL ...") :377-382
"value": "!!!" (string, invalid base64) Err from the base64 decoder :401-403
registered WKT + "value": 123 Err(invalid type: integer ...) :390-393
unregistered + "value": <non-string> Ok with value == [] :404

And the empty payload is the part that survives the round trip: unpack_unchecked / unpack_if on it yields a default Timestamp (1970-01-01T00:00:00Z) or a zero Duration with no error, re-serializing gives {"@type":"...","value":""}, and the input document's actual content is gone with nothing recorded. So a transport that admits a truncated or double-encoded message reports a valid-looking default instead of failing — the value is wrong, but no error surfaces anywhere in the call stack.

Suggested fix

Error on the non-string case instead of defaulting, which makes the fallback branch mean exactly what its comment says (keeping an absent value as an empty payload is a separate call, and Some(Value::Null) probably belongs with "absent"):

None => match obj.remove("value") {
    Some(serde_json::Value::String(s)) => { /* base64 decode, as now */ }
    None | Some(serde_json::Value::Null) => alloc::vec::Vec::new(),
    Some(_) => return Err(serde::de::Error::custom(
        "Any: \"value\" for an unregistered type must be a base64 string")),
},

Roughly four lines plus a regression test asserting is_err() for number / object / array / bool and is_ok() for the valid base64 case, so the test cannot be satisfied by rejecting everything. That is small enough to send as a PR if you agree with the direction — happy to, and I would rather confirm the contract first because it turns currently-Ok documents into Err.

Context

I noticed this while reading buffa-types for the None arm's asymmetry against the registered branches. fallback_base64_without_registry (:858) and fallback_base64_with_registry_but_unknown_type (:895) cover valid base64 strings only, and I found no test asserting the non-string case, so I am reading current behaviour as unintended rather than pinned.

I also saw that #483 rewrites the beginning of this same deserialize to read keys through BufferedObject. This is a different thing — it is about what the value may be, not how keys arrive — so I do not think the two conflict in intent, but I would rebase on that if it lands first.

Dominant language
Rust
Stars
927
Forks
119
Avg merge
3d 6h
Merged PRs (30d)
152

Getting set up

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.

More from anthropics/buffa

All issues in anthropics/buffa

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.