Skip to content

Commit c852f5f

Browse files
goffrieConvex, Inc.
authored andcommitted
Avoid roundtripping into JsonValue when (de)serializing schemas (#36694)
This links up the various `*Json` types (eg. ValidatorJson, TableDefinitionJson, ..) to include each other directly instead of indirecting via `JsonValue` - which requires a recursive copy every time that we call `serde_json::from_value`. Now we do JSON parsing into a typed data structure in one step, then convert the entire thing into the "nice" data structure in a second step. I've also added a new trait `JsonSerializable` to abstract over the `*Json` types to make the pattern clearer. I believe the old code had quadratic time complexity wrt. nesting depth and was a source of perf problems with schema enforcement. GitOrigin-RevId: 52ec985991a68a5a84fb85d403d096a91b3b6e61
1 parent e4b60ef commit c852f5f

File tree

20 files changed

+318
-277
lines changed

20 files changed

+318
-277
lines changed

crates/common/src/bootstrap_model/components/definition.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323
ComponentName,
2424
Reference,
2525
},
26+
json::JsonSerializable as _,
2627
schemas::validator::Validator,
2728
};
2829

@@ -338,7 +339,7 @@ impl TryFrom<ComponentArgumentValidator> for SerializedComponentArgumentValidato
338339
fn try_from(r: ComponentArgumentValidator) -> anyhow::Result<Self> {
339340
Ok(match r {
340341
ComponentArgumentValidator::Value(v) => SerializedComponentArgumentValidator::Value {
341-
value: serde_json::to_string(&JsonValue::try_from(v)?)?,
342+
value: v.json_serialize()?,
342343
},
343344
})
344345
}
@@ -350,9 +351,7 @@ impl TryFrom<SerializedComponentArgumentValidator> for ComponentArgumentValidato
350351
fn try_from(r: SerializedComponentArgumentValidator) -> anyhow::Result<Self> {
351352
Ok(match r {
352353
SerializedComponentArgumentValidator::Value { value: v } => {
353-
ComponentArgumentValidator::Value(Validator::try_from(serde_json::from_str::<
354-
JsonValue,
355-
>(&v)?)?)
354+
ComponentArgumentValidator::Value(Validator::json_deserialize(&v)?)
356355
},
357356
})
358357
}

crates/common/src/bootstrap_model/schema_metadata.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ use serde::{
22
Deserialize,
33
Serialize,
44
};
5-
use serde_json::Value as JsonValue;
65
use value::codegen_convex_serialization;
76

87
use super::schema_state::{
98
SchemaState,
109
SerializedSchemaState,
1110
};
12-
use crate::schemas::DatabaseSchema;
11+
use crate::{
12+
json::JsonSerializable,
13+
schemas::DatabaseSchema,
14+
};
1315

1416
#[derive(Debug, Clone, PartialEq)]
1517
#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))]
@@ -20,13 +22,11 @@ pub struct SchemaMetadata {
2022

2123
impl SchemaMetadata {
2224
pub fn database_schema(&self) -> anyhow::Result<DatabaseSchema> {
23-
let deserialized_value: JsonValue = serde_json::from_str(&self.raw_schema)?;
24-
DatabaseSchema::try_from(deserialized_value)
25+
DatabaseSchema::json_deserialize(&self.raw_schema)
2526
}
2627

2728
pub fn new(state: SchemaState, schema: DatabaseSchema) -> anyhow::Result<Self> {
28-
let json_schema: JsonValue = schema.try_into()?;
29-
let raw_schema = serde_json::to_string(&json_schema)?;
29+
let raw_schema = schema.json_serialize()?;
3030
Ok(Self { state, raw_schema })
3131
}
3232
}

crates/common/src/json/mod.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,62 @@
1+
use std::fmt::Debug;
2+
3+
use anyhow::Context as _;
14
use errors::ErrorMetadata;
25

36
mod expression;
47
mod query;
58
pub use expression::JsonExpression;
9+
use serde::{
10+
de::DeserializeOwned,
11+
Serialize,
12+
};
613

714
#[cfg(test)]
815
mod tests;
916

1017
pub fn invalid_json() -> ErrorMetadata {
1118
ErrorMetadata::bad_request("InvalidJson", "Invalid JSON")
1219
}
20+
21+
/// The standard serde idiom is to directly implement `Serialize` and
22+
/// `Deserialize` on types to make them JSON-serializable.
23+
/// However, this has some downsides, e.g. making it hard to let the JSON & Rust
24+
/// structures differ from each other, and it also means that any custom
25+
/// validation logic has to fit into Serde's error model - which collapses our
26+
/// typed `ErrorMetadata` errors into strings.
27+
///
28+
/// Instead we have a pattern of defining our Rust types, and then defining
29+
/// parallel "*Json" types that implement Serialize/Deserialize (usually
30+
/// derived); this does the first layer of serialization; and then we have
31+
/// TryFrom impls in both directions to do any final validation steps.
32+
///
33+
/// This trait makes it possible to name those "*Json" types uniformly.
34+
pub trait JsonSerializable
35+
where
36+
Self: TryFrom<<Self as JsonSerializable>::Json>,
37+
anyhow::Error: From<<Self as TryFrom<<Self as JsonSerializable>::Json>>::Error>
38+
+ From<<<Self as JsonSerializable>::Json as TryFrom<Self>>::Error>,
39+
{
40+
type Json: Serialize + DeserializeOwned + TryFrom<Self> + Debug;
41+
42+
fn json_serialize(self) -> anyhow::Result<String> {
43+
Ok(serde_json::to_string(&<Self::Json>::try_from(self)?)?)
44+
}
45+
46+
fn json_deserialize(s: &str) -> anyhow::Result<Self> {
47+
Ok(serde_json::from_str::<Self::Json>(s)
48+
.with_context(invalid_json)?
49+
.try_into()?)
50+
}
51+
52+
/// Deserialize from a `serde_json::Value`.
53+
///
54+
/// This should generally not be preferred because `serde_json::Value` is a
55+
/// very dynamic and expensive type to construct. Prefer using `Self::Json`
56+
/// instead.
57+
fn json_deserialize_value(s: serde_json::Value) -> anyhow::Result<Self> {
58+
Ok(serde_json::from_value::<Self::Json>(s)
59+
.with_context(invalid_json)?
60+
.try_into()?)
61+
}
62+
}

0 commit comments

Comments
 (0)