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