fix(fe): handle JSON-stringified fields from backend in message deserialization
Backend returns metadata, ai_moderation_flags, and ai_categories as raw JSON strings (PostgreSQL JSONB cast to string). Frontend expected parsed structs/arrays, causing serde to fail silently — entire PageResult parse failed and user saw empty message list with no error indicator. Add custom deserialize_with handler (from_json_string_or_value) that transparently handles null / direct value / JSON-string cases for the three affected fields.
This commit is contained in:
Generated
+1
@@ -1883,6 +1883,7 @@ name = "shared-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,3 +5,4 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use serde::de::{self, DeserializeOwned, Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ── AI Status ─────────────────────────────────────────────
|
||||
@@ -93,6 +94,50 @@ pub struct ChannelRef {
|
||||
pub thread_name: Option<String>,
|
||||
}
|
||||
|
||||
// ── Helpers: deserialize JSON-string fields ────────────────
|
||||
//
|
||||
// The backend stores certain fields as raw JSON strings in PostgreSQL.
|
||||
// The JSON response therefore contains *stringified* JSON for these fields
|
||||
// (e.g. `"metadata":"{\"stickers\":[]}"`) instead of the actual JSON value.
|
||||
// These helpers transparently parse the string when present, so the frontend
|
||||
// works with the native Rust type regardless of whether the backend ships
|
||||
// a parsed value or a stringified one.
|
||||
|
||||
/// Deserialize a `T` from a JSON value that may be:
|
||||
/// - `null` → `None`
|
||||
/// - a plain JSON value → `Some(T)` (direct serde)
|
||||
/// - a JSON *string* whose *contents* are JSON for `T`
|
||||
fn from_json_string_or_value<'de, T, D>(d: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Intermediate Value to distinguish null / object / array / string
|
||||
let v = Option::<serde_json::Value>::deserialize(d)?;
|
||||
match v {
|
||||
None => Ok(None),
|
||||
Some(serde_json::Value::String(s)) => {
|
||||
serde_json::from_str(&s).map(Some).map_err(de::Error::custom)
|
||||
}
|
||||
Some(json) => serde_json::from_value(json).map(Some).map_err(de::Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete wrapper for `metadata: Option<MessageMetadata>`.
|
||||
pub(crate) fn deser_msg_meta<'de, D: Deserializer<'de>>(
|
||||
d: D,
|
||||
) -> Result<Option<MessageMetadata>, D::Error> {
|
||||
from_json_string_or_value(d)
|
||||
}
|
||||
|
||||
/// Concrete wrapper for `Option<Vec<String>>` fields
|
||||
/// (ai_moderation_flags, ai_categories, etc.).
|
||||
pub(crate) fn deser_str_vec<'de, D: Deserializer<'de>>(
|
||||
d: D,
|
||||
) -> Result<Option<Vec<String>>, D::Error> {
|
||||
from_json_string_or_value(d)
|
||||
}
|
||||
|
||||
// ── Message Record ────────────────────────────────────────
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MessageRecord {
|
||||
@@ -121,13 +166,19 @@ pub struct MessageRecord {
|
||||
pub ai_severity: Option<AiSeverity>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_confidence: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(
|
||||
deserialize_with = "deser_str_vec",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ai_moderation_flags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_moderation_score: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_analysis: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(
|
||||
deserialize_with = "deser_str_vec",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ai_categories: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_recommended_action: Option<AiRecommendedAction>,
|
||||
@@ -135,7 +186,10 @@ pub struct MessageRecord {
|
||||
pub ai_error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_analyzed_at: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(
|
||||
deserialize_with = "deser_msg_meta",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub metadata: Option<MessageMetadata>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user