It would be helpful to have a type similar to Go's json.RawMessage that is not tokenized during deserialization, but rather its raw contents stored as a Vec<u8> or &'de [u8].
The following pseudo-code demonstrates the idea.
#[derive(Deserialize)]
struct Struct {
/// Deserialized normally.
core_data: Vec<i32>,
/// Contents of `user_data` are copied / borrowed directly from the input
/// with no modification.
///
/// `RawValue<'static>` is akin to `Vec<u8>`.
/// `RawValue<'a>` is akin to `&'a [u8]`.
user_data: serde_json::RawValue<'static>,
}
fn main() {
let json = r#"
{
"core_data": [1, 2, 3],
"user_data": { "foo": {}, "bar": 123, "baz": "abc" }
}
"#;
let s: Struct = serde_json::from_bytes(&json).unwrap();
println!("{}", s.user_data); // "{ \"foo\": {}, \"bar\": 123, \"baz\": \"abc\" }"
}
The main advantage of this is would be to have 'lazily-deserialized' values.
@dtolnay mentions that the work happening in #416 might lay some groundwork for such a change to take place.
Yeah, this sounds useful. Ideally we could get some of the building blocks for this sort of thing (custom 'built-in' types) integrated into serde itself, in time.
I need this for a project I'm working on, and would be happy to give it shot following the same approach as #416. Is this something that you'd accept a PR for?
Yes I would like a PR for this. When you say you need this feature, is it because deserializing to serde_json::Value is too slow in your project and you are expecting this to have better performance, or some other reason?
Yeah, I've got an http web server that's backed by a postgres database which stores JSONB values, and would like to shuffle data back and forth (and embed into other payloads) with as few allocations as possible.
As a first step, please put together a short benchmark that you trust as reasonably representative of the shuffling data back and forth piece of your application. Ideally it would depend on serde / serde_derive / serde_json but nothing else -- so no postgres or http stuff. Focus on the data structures so that they have the same ratio and pattern of raw vs meaningful content as your real use case. Write it using serde_json::Value for now, and then the goal will be to swap in a raw value once it's implemented and observe a speedup.
Wrote a small microbenchmark here: https://gist.github.com/srijs/7fe21b43c2cf5d2ceeb593ae4591c6f5
Please let me know if this is what you had in mind!
:+1: looks good, let's make it work.
I released one possible implementation of this in serde_json_experimental 1.0.29-rc1. Here is the documentation. I would be interested in hearing people's experience with this API before committing to it.
[dependencies]
serde_json_experimental = { version = "1.0.29-rc1", features = ["raw_value"] }
From the linked PR:
using the RawValue type instead of deserializing/serializing via Value results in a 3-4x speed-up.
test deserialize_and_serialize_raw_value ... bench: 1,759 ns/iter (+/- 354) test deserialize_and_serialize_value ... bench: 6,091 ns/iter (+/- 1,776)
@dtolnay will this be behind a feature flag when released in serde_json proper as well? I'm worried that it might be difficult to get other crates (such as postgres) to add support for RawValue when it's feature-gated like this.
I would like for this to be feature gated because it adds code to the critical path of applications even that do not use RawValue. In what ways do you see crates needing to add support for RawValue where the feature gate would be an obstacle?
That makes sense to me from a performance standpoint.
I'm not super familiar with how feature flags interact in cargo, but my concern is that in order to e.g. add an impl ToSql for RawValue, postgres needs to opt-in into the raw_value feature. What does that mean for crates wanting to use this impl? Do they need also need to enable the raw_value feature? Will they get type errors since the serde_json crate that postgres depends on (and exposes impls on) is be different from the serde_json dependency that their crate has?
On a different note, I've been giving the experimental release a shot in my project, and it's been working well so far. The only thing I ran into that wasn't easy to solve was converting a String into a Box<RawValue>. To solve this, I've used owning_ref for now, but that was a bit cumbersome and has it's own drawbacks. Do you think it might be worth adding a Box<str> -> Result<Box<RawValue>> method to cover this use-case?
Do they need also need to enable the
raw_valuefeature
No; Cargo features are globally additive.
As @shepmaster wrote, I believe Cargo handles that situation correctly. If the postgres crate enables the feature and provides that impl, any crate that depends on postgres can use the impl.
Regarding Box<str> -> Result<Box<RawValue>>, is this needed due to a performance bottleneck? Otherwise you would write serde_json::from_str::<Box<RawValue>>(&string). It should be fine to provide something like the following constructor but I would want to know how much faster it is than from_str::<Box<RawValue>> in practice.
impl RawValue {
pub fn from_string(json: String) -> Result<Box<Self>> {
{
let borrowed = ::from_str::<&Self>(&json)?;
if borrowed.json.len() < json.len() {
return Ok(borrowed.to_owned());
}
}
Ok(Self::from_owned(json.into_boxed_str()))
}
}
Thanks, I wasn't quite sure, but it sounds like cargo will do the right thing 馃憤
Yeah, I'd like to avoid the extra malloc + memcpy if possible. Haven't had a chanche to benchmark it yet, just wanted to prevent it from becoming a bottleneck for larger documents in the first place.
Released in 1.0.29: https://docs.rs/serde_json/1.0/serde_json/value/struct.RawValue.html
Most helpful comment
Released in 1.0.29: https://docs.rs/serde_json/1.0/serde_json/value/struct.RawValue.html