Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions sea-orm-macros/src/derives/entity_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub fn expand_derive_entity_model(
let mut model_ex = false;
let mut rename_all: Option<CaseStyle> = None;
let mut serde_rename_all: Option<CaseStyle> = None;
let mut serde_rename_all_serialize: Option<CaseStyle> = None;

// Parse #[serde(rename_all = "...")] at struct level
attrs
Expand All @@ -80,12 +81,16 @@ pub fn expand_derive_entity_model(
if let Ok(lit) = meta.value().and_then(|v| v.parse::<LitStr>()) {
// #[serde(rename_all = "camelCase")]
serde_rename_all = CaseStyle::from_str(&lit.value()).ok();
serde_rename_all_serialize = serde_rename_all;
} else {
// #[serde(rename_all(serialize = "...", deserialize = "..."))]
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("deserialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename_all = CaseStyle::from_str(&lit.value()).ok();
} else if nested.path.is_ident("serialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename_all_serialize = CaseStyle::from_str(&lit.value()).ok();
} else {
consume_meta(nested);
}
Expand Down Expand Up @@ -169,6 +174,8 @@ pub fn expand_derive_entity_model(
let mut auto_increment: Option<bool> = None;
#[cfg(feature = "with-json")]
let mut columns_json_keys: Punctuated<_, Comma> = Punctuated::new();
#[cfg(feature = "with-json")]
let mut columns_json_keys_serialize: Punctuated<_, Comma> = Punctuated::new();

if table_iden {
if let Some(table_name) = &table_name {
Expand Down Expand Up @@ -211,6 +218,8 @@ pub fn expand_derive_entity_model(
let mut seaography_ignore = false;
#[cfg(feature = "with-json")]
let mut serde_rename: Option<String> = None;
#[cfg(feature = "with-json")]
let mut serde_rename_serialize: Option<String> = None;

let mut column_name = if let Some(case_style) = rename_all {
Some(field_name.convert_case(Some(case_style)))
Expand Down Expand Up @@ -356,12 +365,16 @@ pub fn expand_derive_entity_model(
{
// #[serde(rename = "xxx")]
serde_rename = Some(lit.value());
serde_rename_serialize = serde_rename.clone();
} else {
// #[serde(rename(serialize = "...", deserialize = "..."))]
meta.parse_nested_meta(|nested| {
if nested.path.is_ident("deserialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename = Some(lit.value());
} else if nested.path.is_ident("serialize") {
let lit: LitStr = nested.value()?.parse()?;
serde_rename_serialize = Some(lit.value());
} else {
consume_meta(nested);
}
Expand All @@ -383,6 +396,13 @@ pub fn expand_derive_entity_model(
serde_rename_all,
);

#[cfg(feature = "with-json")]
let json_key_name_serialize = serde_deserialize_name(
&original_field_name,
serde_rename_serialize.as_deref(),
serde_rename_all_serialize,
);

if let Some(enum_name) = enum_name {
field_name = enum_name;
}
Expand Down Expand Up @@ -416,6 +436,11 @@ pub fn expand_derive_entity_model(
columns_json_keys.push(quote! {
Self::#field_name => #json_key_name
});

#[cfg(feature = "with-json")]
columns_json_keys_serialize.push(quote! {
Self::#field_name => #json_key_name_serialize
});
}

if is_primary_key {
Expand Down Expand Up @@ -579,6 +604,12 @@ pub fn expand_derive_entity_model(
#columns_json_keys
}
}

fn serialize_json_key(&self) -> &'static str {
match self {
#columns_json_keys_serialize
}
}
}

#[cfg(not(feature = "with-json"))]
Expand Down
8 changes: 7 additions & 1 deletion sea-orm-sync/src/entity/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,17 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
cast_enum_as(val, &self.def(), save_enum_as)
}

/// JSON key used for this column when (de)serializing the model.
/// JSON key used for this column when deserializing the model.
#[cfg(feature = "with-json")]
fn json_key(&self) -> &'static str {
self.as_str()
}

/// JSON key used for this column when serializing the model.
#[cfg(feature = "with-json")]
fn serialize_json_key(&self) -> &'static str {
self.as_str()
}
}

/// Extension methods on [`ColumnType`] for building [`ColumnDef`]s and
Expand Down
34 changes: 25 additions & 9 deletions src/entity/active_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,22 +508,38 @@ pub trait ActiveModelTrait: Clone + Debug {
)));
};

let dummy_am = Self::default_values();
let dummy_model = Self::default_values().try_into_model().map_err(json_err)?;
let dummy_value = serde_json::to_value(dummy_model).map_err(json_err)?;
let serde_json::Value::Object(dummy_value) = dummy_value else {
return Err(DbErr::Json(format!(
"invalid type: expected JSON object for dummy model for {}",
<<Self as ActiveModelTrait>::Entity as IdenStatic>::as_str(&Default::default())
)));
};

let ser_de_map = <<Self::Entity as EntityTrait>::Column>::iter()
.map(|col| (col.serialize_json_key(), col.json_key()))
.collect::<std::collections::HashMap<_, _>>();

let mut merged = dummy_value
.into_iter()
.map(|(key, val)| {
// map seralized keys into deserialize keys
let new_key = ser_de_map
.get(key.as_str())
.map(ToString::to_string)
.unwrap_or(key);
(new_key, val)
})
.collect::<serde_json::Map<_, _>>();

let len = <<Self::Entity as EntityTrait>::Column>::iter().len();
// Mark down which attribute exists in the JSON object
let mut json_keys = Vec::with_capacity(len);
let mut merged = serde_json::Map::with_capacity(len);

for col in <<Self::Entity as EntityTrait>::Column>::iter() {
let key = col.json_key();
let has_key = input.contains_key(key);
json_keys.push((col, has_key));
match dummy_am.get(col) {
ActiveValue::Unchanged(value) | ActiveValue::Set(value) => {
merged.insert(key.to_owned(), sea_query::sea_value_to_json_value(&value));
}
_ => {}
}
}

merged.append(&mut input);
Expand Down
8 changes: 7 additions & 1 deletion src/entity/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,17 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
cast_enum_as(val, &self.def(), save_enum_as)
}

/// JSON key used for this column when (de)serializing the model.
/// JSON key used for this column when deserializing the model.
#[cfg(feature = "with-json")]
fn json_key(&self) -> &'static str {
self.as_str()
}

/// JSON key used for this column when serializing the model.
#[cfg(feature = "with-json")]
fn serialize_json_key(&self) -> &'static str {
self.as_str()
}
}

/// Extension methods on [`ColumnType`] for building [`ColumnDef`]s and
Expand Down
102 changes: 102 additions & 0 deletions src/tests_cfg/serde_datetimes_active_models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/// Regression test for [https://github.com/SeaQL/sea-orm/issues/3175]
/// `ActiveModel::from_json` failed to deserialize time columns to `NotSet`
/// when the field was missing from the JSON payload, because the
/// trait-default implementation round-tripped through the model after
/// merging SQL-literal dummy values.

#[cfg(feature = "with-time")]
mod time_model {
use crate as sea_orm;
use sea_orm::entity::prelude::*;
use serde::{Serialize, Deserialize};

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "time")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: i32,
#[serde(with = "time::serde::timestamp")]
pub created_at: TimeDateTimeWithTimeZone,
}

impl ActiveModelBehavior for ActiveModel {}
}

#[cfg(feature = "with-chrono")]
mod chrono_model {
use crate as sea_orm;
use sea_orm::entity::prelude::*;
use serde::{Serialize, Deserialize};

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "time")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: i32,
#[serde(rename = "tstamp", with = "chrono::serde::ts_seconds")]
pub created_at: ChronoDateTimeUtc,
}

impl ActiveModelBehavior for ActiveModel {}
}

mod test {
use super::{time_model::ActiveModel as TimeAM, chrono_model::ActiveModel as ChronoAM};
use crate::{ActiveValue, entity::ActiveModelTrait};

#[test]
#[cfg(feature = "with-time")]
fn test_from_json_missing_time_field_is_not_set() {
let json = serde_json::json!({
"id": 1,
});

let am = TimeAM::from_json(json).unwrap();

assert_eq!(am.id, ActiveValue::Set(1));
assert_eq!(am.created_at, ActiveValue::NotSet);
}

#[test]
#[cfg(feature = "with-time")]
fn test_from_json_present_time_field_is_set() {
let json = serde_json::json!({
"id": 1,
"created_at": 1704067200,
});

let am = TimeAM::from_json(json).unwrap();

assert_eq!(am.id, ActiveValue::Set(1));
assert!(matches!(am.created_at, ActiveValue::Set(_)));
}

#[test]
#[cfg(feature = "with-chrono")]
fn test_from_json_missing_chrono_field_is_not_set() {
let json = serde_json::json!({
"id": 1,
});

let am = ChronoAM::from_json(json).unwrap();

assert_eq!(am.id, ActiveValue::Set(1));
assert_eq!(am.created_at, ActiveValue::NotSet);
}

#[test]
#[cfg(feature = "with-chrono")]
fn test_from_json_present_chrono_field_is_set() {
let json = serde_json::json!({
"id": 1,
"tstamp": 1704067200,
});

let am = ChronoAM::from_json(json).unwrap();

assert_eq!(am.id, ActiveValue::Set(1));
assert!(matches!(am.created_at, ActiveValue::Set(_)));
}
}
Loading