Skip to main content

cratestack_sqlx/
json.rs

1//! Postgres `jsonb` (de)serialization for schema-declared `Json` columns
2//! (cratestack#162).
3//!
4//! `sqlx::types::Json<T>` — what generated model structs used before this
5//! fix — round-trips `T` through `T`'s own `Serialize`/`Deserialize`. At
6//! the time of cratestack#162, `T = cratestack_core::Value` derived an
7//! externally-tagged `Serialize`/`Deserialize` (serde's default for a
8//! data-carrying enum), so a column ended up holding `{"Map": {}}` instead
9//! of `{}`, `{"List": [...]}` instead of `[...]`, and so on. That broke
10//! reading any jsonb cratestack didn't write itself (legacy rows, other
11//! writers, manual inserts — they hold *plain* JSON) and broke native
12//! jsonb operator queries (`->`/`->>`) against the column, since the real
13//! value sat nested under a variant tag.
14//!
15//! [`Json`] is a from-scratch local newtype (not a re-export of
16//! `sqlx::types::Json`, and not `cratestack_core::Json` either — both are
17//! foreign types here, so implementing `sqlx::Type`/`Encode`/`Decode` for
18//! either would violate Rust's orphan rules) whose Postgres impls convert
19//! through [`cratestack_core::Value::to_plain_json`] /
20//! [`cratestack_core::Value::from_plain_json`] instead: the untagged,
21//! natural JSON shape. As of cratestack#506, `Value`'s own hand-written
22//! `Serialize`/`Deserialize` (in `cratestack_core::value::codec`) is
23//! *also* untagged on every wire, not just this jsonb column path — see
24//! that module's doc for why.
25//!
26//! The actual jsonb wire format (the leading version byte on binary-format
27//! values, `JSON` vs `JSONB` OID dispatch) is delegated to
28//! `sqlx::types::Json<serde_json::Value>`, which already gets this right —
29//! this module only owns the `Value <-> serde_json::Value` conversion.
30
31use cratestack_core::Value;
32use serde::{Deserialize, Serialize};
33
34use crate::sqlx::encode::IsNull;
35use crate::sqlx::error::BoxDynError;
36use crate::sqlx::postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres};
37use crate::sqlx::types::Json as SqlxJson;
38use crate::sqlx::{Decode, Encode, Type};
39
40/// Wrapper for a schema-declared `Json` column's Rust field type. See the
41/// module docs for why this exists instead of `sqlx::types::Json<Value>`.
42///
43/// `Serialize`/`Deserialize` stay `#[serde(transparent)]` — delegating
44/// straight to `T`'s own impl (for `T = Value`, untagged since
45/// cratestack#506) — so the model struct's HTTP/RPC wire representation is
46/// unchanged from before this fix. The *jsonb column* codec below
47/// (`sqlx::Type` / `Encode` / `Decode`, used for the Postgres bind/
48/// row-decode path, never for the wire format) is untagged too; that's
49/// the actual cratestack#162 fix, scoped to on-disk storage and
50/// independent of `Value`'s own serde impls.
51#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
52#[serde(transparent)]
53pub struct Json<T>(pub T);
54
55impl<T> Json<T> {
56    pub fn into_inner(self) -> T {
57        self.0
58    }
59}
60
61impl<T> From<T> for Json<T> {
62    fn from(value: T) -> Self {
63        Json(value)
64    }
65}
66
67impl<T> std::ops::Deref for Json<T> {
68    type Target = T;
69    fn deref(&self) -> &T {
70        &self.0
71    }
72}
73
74impl<T> std::ops::DerefMut for Json<T> {
75    fn deref_mut(&mut self) -> &mut T {
76        &mut self.0
77    }
78}
79
80impl Type<Postgres> for Json<Value> {
81    fn type_info() -> PgTypeInfo {
82        <SqlxJson<serde_json::Value> as Type<Postgres>>::type_info()
83    }
84
85    fn compatible(ty: &PgTypeInfo) -> bool {
86        <SqlxJson<serde_json::Value> as Type<Postgres>>::compatible(ty)
87    }
88}
89
90impl PgHasArrayType for Json<Value> {
91    fn array_type_info() -> PgTypeInfo {
92        <SqlxJson<serde_json::Value> as PgHasArrayType>::array_type_info()
93    }
94
95    fn array_compatible(ty: &PgTypeInfo) -> bool {
96        <SqlxJson<serde_json::Value> as PgHasArrayType>::array_compatible(ty)
97    }
98}
99
100impl<'q> Encode<'q, Postgres> for Json<Value> {
101    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
102        SqlxJson(self.0.to_plain_json()).encode_by_ref(buf)
103    }
104}
105
106impl<'r> Decode<'r, Postgres> for Json<Value> {
107    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
108        let SqlxJson(plain) = <SqlxJson<serde_json::Value> as Decode<'r, Postgres>>::decode(value)?;
109        Ok(Json(Value::from_plain_json(plain)))
110    }
111}