-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optional fields with #[serde(default)]
for deserialization
#175
Comments
Use |
For individual fields labeled as |
You could do it like this: use serde::Serialize;
#[derive(TS, Serialize)]
#[ts(export)]
struct Foo {
#[ts(optional)]
#[serde(skip_serializing_if = "should_skip", default = "Default::default")]
my_optional_bool: Option<bool>,
}
fn should_skip(field: &Option<bool>) -> bool {
matches!(field, None | Some(false))
} |
Right, this is what we were doing before. However, because Rust doesn't have a |
I think this is data modeling error. You're saying your data should only have two valid states, but it has three. Something more appropriate would be using use ts_rs::TS;
use serde::{Deserialize, Serialize};
#[derive(TS, Serialize, Deserialize, PartialEq, Debug)]
#[ts(export)]
struct Foo {
#[ts(optional, as = "Option<bool>")]
#[serde(skip_serializing_if = "Option::is_none", default = "Default::default", with = "deser")]
my_optional_bool: Option<()>,
}
mod deser {
use serde::{Serializer, Serialize, Deserializer, Deserialize};
pub fn serialize<S: Serializer>(value: &Option<()>, serializer: S) -> Result<S::Ok, S::Error> {
value.map(|_| true).serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<()>, D::Error> {
Ok(Option::<bool>::deserialize(deserializer)?.filter(|&x| x).map(|_| ()))
}
}
#[test]
fn test() {
let none = Foo { my_optional_bool: None };
let some = Foo { my_optional_bool: Some(()) };
// Type definition
assert_eq!(Foo::inline(), "{ my_optional_bool?: boolean, }");
// Serializing
assert_eq!(serde_json::to_string(&none).unwrap(), "{}");
assert_eq!(serde_json::to_string(&some).unwrap(), r#"{"my_optional_bool":true}"#);
// Deserializing
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":true}"#).unwrap(), some);
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":false}"#).unwrap(), none); // `false` becomes `None`!
assert_eq!(serde_json::from_str::<Foo>("{}").unwrap(), none);
} |
If you really want to use match optional_bool {
Some(true) => todo!("True case"),
Some(false) | None => todo!("False case")
} or if optional_bool.filter(|&x| x).is_some() {
todo!("True case"),
} else {
todo!("False case")
} or something else that handles all three cases |
My usecase is one where nonexistence of a value is equivalent to passing that value as false. Serde allows that using the default macro. In our code, we treat the value as boolean because there is either true or [false/undefined]. We don’t want to use Option for the very reason you described — it creates a third state in the data model. Unfortunately, ts_rs only allows for optional fields to be declared as Option. If Rust had a “true” object, then we could do Option or just Option<()> however I prefer that our rust code use boolean rather than Option<()>. For me, it makes complete sense for a non-Option field with #[serde(default)] to be rendered as “field?: type” in ts. I would support a feature that made that conversion possible. |
We've had something somewhat similar in a previous version, but ended up removing it. Like So yeah, this all gets kinda tricky. |
In my mind ts_rs is a code generator, but doesn’t explicitly guarantee that the generated data is consistent or logical. So for our case, generating an optional parameter with a question mark should be something the user can do at their own behest. In typescript, a parameter that is declared optional can be removed from the instantiation, but one declared I think a ts(optional) would be great that supports arbitrary objects. And yes, if we were to be fully bidirectionally consistent then we would need to use serde(default) and skip_serializing_if(x). But that’s our own responsibility IMO. |
Fair enough! I've noted this in #294.
|
Given that ts optional is only available for Option types, this won’t be a breaking release if the functionality is kept as is for option types. In fact it should basically “just work” if you remove the requirement for Option in the macro. Can submit a PR if helpful |
I think it's important that Only So right now, we could support |
Then this becomes even simpler, no custom (de)serialization required: use ts_rs::TS;
use serde::{Deserialize, Serialize};
#[derive(TS, Serialize, Deserialize, PartialEq, Debug)]
#[ts(export)]
struct Foo {
#[ts(optional, as = "Option<bool>")]
#[serde(skip_serializing_if = "std::ops::Not::not", default = "Default::default")]
my_optional_bool: bool,
}
#[test]
fn test() {
let falsy = Foo { my_optional_bool: false };
let truthy = Foo { my_optional_bool: true };
// Type definition
assert_eq!(Foo::inline(), "{ my_optional_bool?: boolean, }");
// Serializing
assert_eq!(serde_json::to_string(&falsy).unwrap(), "{}"); // `false` is not serialized
assert_eq!(serde_json::to_string(&truthy).unwrap(), r#"{"my_optional_bool":true}"#);
// Deserializing
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":true}"#).unwrap(), truthy);
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":false}"#).unwrap(), falsy);
assert_eq!(serde_json::from_str::<Foo>("{}").unwrap(), falsy); // `undefined` is deserialized into `false`
} |
With The feature you're asking for already exists through |
That's a very nice solution! That didn't even cross my mind. |
This is a good intermediate solution for sure! It would be nice to have a solution that can use the type as defined, so that there's no duplication between But I appreciate that recommendation, I can implement that solution now. |
As a real example, here's my (now updated) definition of a struct: #[derive(Debug, Deserialize, Serialize, TS, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "ts/")]
pub struct FilePropertySelection {
#[ts(optional, as = "Option<bool>")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub everything: bool,
/// Return all properties within the given groups (along with properties)
#[ts(optional, as = "Option<HashSet<EntityPropertyGroupId>>")]
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub groups: HashSet<EntityPropertyGroupId>,
/// Return all given properties (along with groups)
/// WARN: If a property is specified, and it doesn't exist, and error will
/// occur
#[ts(optional, as = "Option<HashSet<FilePropertyId>>")]
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub properties: HashSet<FilePropertyId>,
} As you can see, the use of |
We could allow use ts_rs::TS;
use serde::{Deserialize, Serialize};
#[derive(TS, Serialize, Deserialize, PartialEq, Debug)]
#[ts(export)]
struct Foo {
#[ts(optional, as = "Option<_>")]
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
my_optional_bool: bool,
}
#[test]
fn test() {
let falsy = Foo { my_optional_bool: false };
let truthy = Foo { my_optional_bool: true };
// Type definition
assert_eq!(Foo::inline(), "{ my_optional_bool?: boolean, }");
// Serializing
assert_eq!(serde_json::to_string(&falsy).unwrap(), "{}"); // `false` is not serialized
assert_eq!(serde_json::to_string(&truthy).unwrap(), r#"{"my_optional_bool":true}"#);
// Deserializing
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":true}"#).unwrap(), truthy);
assert_eq!(serde_json::from_str::<Foo>(r#"{"my_optional_bool":false}"#).unwrap(), falsy);
assert_eq!(serde_json::from_str::<Foo>("{}").unwrap(), falsy); // `undefined` is deserialized into `false`
} |
Your example would then become #[derive(Debug, Deserialize, Serialize, TS, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "ts/")]
pub struct FilePropertySelection {
#[ts(optional, as = "Option<_>")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub everything: bool,
/// Return all properties within the given groups (along with properties)
#[ts(optional, as = "Option<_>")]
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub groups: HashSet<EntityPropertyGroupId>,
/// Return all given properties (along with groups)
/// WARN: If a property is specified, and it doesn't exist, and error will
/// occur
#[ts(optional, as = "Option<_>")]
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
pub properties: HashSet<FilePropertyId>,
} |
Sounds great! |
I still think the best/easiest solution is to just allow for the |
Great! I have already coded it on my work computer and will submit a PR on monday |
Check out #299 |
Hi thanks for the library!
For a struct with serde default for some fields, the fields becomes optional when Deserializing and if not define will use the default value. I think the resulting TypeScript code should reflect that. The following Rust code:
Should result in the following TypeScript types if we only care about deserialization
The text was updated successfully, but these errors were encountered: