diff --git a/credential-exchange-format/CHANGELOG.md b/credential-exchange-format/CHANGELOG.md index 1931629..1bbc4b0 100644 --- a/credential-exchange-format/CHANGELOG.md +++ b/credential-exchange-format/CHANGELOG.md @@ -20,6 +20,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **BREAKING**: Changed `integration_hash` to `integrity_hash` in `FileCredential`. (#87) - **BREAKING**: Renamed `ty` to `type` in serialized representations of `Credential::Unknown`. (#125) +- **BREAKING**: Field values are now using a new type to encode whether the field was parsed as the + expected field type, or whether the field was of the wrong type. (#127) ### Fixed diff --git a/credential-exchange-format/src/document.rs b/credential-exchange-format/src/document.rs index 6f3b356..45d6894 100644 --- a/credential-exchange-format/src/document.rs +++ b/credential-exchange-format/src/document.rs @@ -65,13 +65,13 @@ mod tests { fields: vec![ EditableFieldValue::<()>::String(EditableField { id: Some(B64Url::from(b"field1".as_slice())), - value: EditableFieldString("hello".into()), + value: EditableFieldString("hello".into()).into(), label: None, extensions: None, }), EditableFieldValue::<()>::Boolean(EditableField { id: None, - value: EditableFieldBoolean(false), + value: EditableFieldBoolean(false).into(), label: None, extensions: None, }), @@ -129,21 +129,21 @@ mod tests { match &credential.fields[0] { EditableFieldValue::String(field) => { - assert_eq!(field.value.0, "hello"); + assert_eq!(field.value.as_expected().map(|v| v.0.as_str()), Ok("hello")); } _ => panic!("Expected string field"), } match &credential.fields[1] { EditableFieldValue::ConcealedString(field) => { - assert_eq!(field.value.0, "world"); + assert_eq!(field.value.as_expected().map(|v| v.0.as_str()), Ok("world")); } _ => panic!("Expected concealed string field"), } match &credential.fields[2] { EditableFieldValue::Boolean(field) => { - assert!(!field.value.0); + assert_eq!(field.value.as_expected().map(|e| e.0), Ok(false)); } _ => panic!("Expected boolean field"), } @@ -151,11 +151,11 @@ mod tests { match &credential.fields[3] { EditableFieldValue::YearMonth(field) => { assert_eq!( - field.value, - EditableFieldYearMonth { + field.value.as_expected(), + Ok(&EditableFieldYearMonth { year: 2025, month: Month::February, - } + }) ); } _ => panic!("Expected boolean field"), diff --git a/credential-exchange-format/src/editable_field.rs b/credential-exchange-format/src/editable_field.rs index 764aa15..e605fb6 100644 --- a/credential-exchange-format/src/editable_field.rs +++ b/credential-exchange-format/src/editable_field.rs @@ -2,7 +2,10 @@ use std::{borrow::Cow, fmt, str}; use chrono::{Month, NaiveDate}; use serde::{ - de::{DeserializeOwned, Visitor}, + de::{ + value::{StrDeserializer, StringDeserializer}, + DeserializeOwned, Visitor, + }, ser::SerializeStruct, Deserialize, Serialize, }; @@ -15,7 +18,7 @@ pub struct EditableField { /// sequence with a maximum size of 64 bytes. It SHOULD NOT be displayed to the user. pub id: Option, /// This member contains the fieldType defined by the user. - pub value: T, + pub value: Expected, /// This member contains a user facing value describing the value stored. This value MAY be /// user defined. pub label: Option, @@ -25,6 +28,143 @@ pub struct EditableField { pub extensions: Option>>, } +/// A field of the incorrect type that was passed instead of an expected field type. +/// +/// For example if the spec requires a `string` type, but the user passes in +/// `concealed-string` instead. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum UnexpectedField { + String(EditableFieldString), + ConcealedString(EditableFieldConcealedString), + Boolean(EditableFieldBoolean), + Date(EditableFieldDate), + YearMonth(EditableFieldYearMonth), + SubdivisionCode(EditableFieldSubdivisionCode), + CountryCode(EditableFieldCountryCode), + WifiNetworkSecurityType(EditableFieldWifiNetworkSecurityType), + Email(EditableFieldEmail), + Number(EditableFieldNumber), + Unknown { + /// The unexpected field type. + field_type: FieldType, + /// The Unknown string passed as a value of type `field_type`. + /// + /// Example: a `date` that doesn't conform to `yyyy-mm-dd`. + value: String, + }, +} + +impl UnexpectedField { + /// Returns the [`FieldType`] for this value. + #[inline] + pub fn field_type(&self) -> FieldType { + match self { + Self::String(_) => EditableFieldString::field_type(), + Self::ConcealedString(_) => EditableFieldConcealedString::field_type(), + Self::Boolean(_) => EditableFieldBoolean::field_type(), + Self::Date(_) => EditableFieldDate::field_type(), + Self::YearMonth(_) => EditableFieldYearMonth::field_type(), + Self::SubdivisionCode(_) => EditableFieldSubdivisionCode::field_type(), + Self::CountryCode(_) => EditableFieldCountryCode::field_type(), + Self::WifiNetworkSecurityType(_) => EditableFieldWifiNetworkSecurityType::field_type(), + Self::Email(_) => EditableFieldEmail::field_type(), + Self::Number(_) => EditableFieldNumber::field_type(), + Self::Unknown { field_type, .. } => field_type.clone(), + } + } +} + +impl From for String { + #[inline] + fn from(value: UnexpectedField) -> Self { + match value { + UnexpectedField::String(v) => v.0, + UnexpectedField::ConcealedString(v) => v.0, + UnexpectedField::WifiNetworkSecurityType(v) => v.into(), + UnexpectedField::SubdivisionCode(v) => v.0, + UnexpectedField::CountryCode(v) => v.0, + UnexpectedField::Unknown { value: v, .. } => v, + UnexpectedField::Email(v) => v.0, + UnexpectedField::Number(v) => v.0, + UnexpectedField::Boolean(v) => v.into(), + UnexpectedField::Date(v) => v.into(), + UnexpectedField::YearMonth(v) => v.into(), + } + } +} + +/// Holds onto an editable field, and records whether it was an expected or unexpected field. +#[derive(Clone, Debug, PartialEq, Eq)] +enum ExpectedInner { + /// The field we found had the same field type we expected. + Expected(T), + /// The field we found had a different field type than what we expected. + Unexpected(UnexpectedField), +} + +/// Holds onto an editable field, and records whether it was an expected or unexpected field. +/// +/// This is used as a type-safe wrapper around an editable field to encode expectations around +/// the type of field we are expecting in a credential. +/// +/// This can only be instantiated via the exposed `From` implementation so that newly +/// constructed credentials remain spec-compliant with regards to their fields. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Expected(ExpectedInner); + +impl Expected +where + T: EditableFieldType, +{ + /// Returns the real [`FieldType`] of this field. + #[inline] + pub fn field_type(&self) -> FieldType { + match &self.0 { + ExpectedInner::Expected(_) => T::field_type(), + ExpectedInner::Unexpected(f) => f.field_type(), + } + } + + /// Tries to return the field type we were expecting. + #[inline] + pub fn as_expected(&self) -> Result<&T, &UnexpectedField> { + match &self.0 { + ExpectedInner::Expected(t) => Ok(t), + ExpectedInner::Unexpected(f) => Err(f), + } + } + + /// Tries to return the field type we were expecting as an owned type. + #[inline] + pub fn into_expected(self) -> Result { + match self.0 { + ExpectedInner::Expected(t) => Ok(t), + ExpectedInner::Unexpected(f) => Err(f), + } + } +} + +impl From for Expected { + #[inline] + fn from(t: T) -> Self { + Self(ExpectedInner::Expected(t)) + } +} + +impl From> for String +where + T: Into, +{ + #[inline] + fn from(value: Expected) -> Self { + match value.0 { + ExpectedInner::Expected(t) => t.into(), + ExpectedInner::Unexpected(f) => f.into(), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] @@ -62,7 +202,7 @@ pub enum FieldType { /// A trait to associate the field structs with their `field_type` tag. pub trait EditableFieldType { /// The `field_type` value associated with the type - fn field_type(&self) -> FieldType; + fn field_type() -> FieldType; } impl Serialize for EditableField @@ -87,7 +227,29 @@ where } state.serialize_field("fieldType", &self.value.field_type())?; - state.serialize_field("value", &self.value)?; + match &self.value.0 { + ExpectedInner::Expected(t) => { + state.serialize_field("value", &t)?; + } + ExpectedInner::Unexpected(t) => match t { + UnexpectedField::String(v) => state.serialize_field("value", &v)?, + UnexpectedField::ConcealedString(v) => state.serialize_field("value", &v)?, + UnexpectedField::WifiNetworkSecurityType(v) => { + state.serialize_field("value", &v)? + } + UnexpectedField::SubdivisionCode(v) => state.serialize_field("value", &v)?, + UnexpectedField::CountryCode(v) => state.serialize_field("value", &v)?, + UnexpectedField::Unknown { value: v, .. } => state.serialize_field("value", &v)?, + UnexpectedField::Boolean(b) => { + let v = if b.0 { "true" } else { "false" }; + state.serialize_field("value", v)? + } + UnexpectedField::Date(date) => state.serialize_field("value", &date)?, + UnexpectedField::YearMonth(v) => state.serialize_field("value", &v)?, + UnexpectedField::Email(v) => state.serialize_field("value", &v)?, + UnexpectedField::Number(v) => state.serialize_field("value", &v)?, + }, + } if let Some(ref label) = self.label { state.serialize_field("label", label)?; @@ -109,6 +271,25 @@ where } } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EditableFieldHelper { + #[serde(default)] + id: Option, + value: String, + field_type: FieldType, + #[serde(default)] + label: Option, + #[serde(default = "none::")] + extensions: Option>>, +} + +// Need to use this instead of the normal default, +// otherwise the derive creates a `E: Default` constraint. +fn none() -> Option>> { + None +} + impl<'de, T, E> Deserialize<'de> for EditableField where T: EditableFieldType + DeserializeOwned, @@ -118,35 +299,62 @@ where where D: serde::Deserializer<'de>, { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct EditableFieldHelper { - #[serde(default)] - id: Option, - value: T, - field_type: FieldType, - #[serde(default)] - label: Option, - #[serde(default = "none::")] - extensions: Option>>, - } - // Need to use this instead of the normal default, - // otherwise the derive creates a `E: Default` constraint. - fn none() -> Option>> { - None - } + let helper: EditableFieldHelper = EditableFieldHelper::deserialize(deserializer)?; + + let value = if T::field_type() == helper.field_type { + match T::deserialize(StrDeserializer::::new( + helper.value.as_str(), + )) { + Ok(t) => ExpectedInner::Expected(t), + Err(_) => ExpectedInner::Unexpected(UnexpectedField::Unknown { + field_type: helper.field_type, + value: helper.value, + }), + } + } else { + macro_rules! deserialize_from_str { + ($t:tt => $variant:ident) => { + match $t::deserialize(StrDeserializer::::new(&helper.value)) { + Ok(v) => UnexpectedField::$variant(v), + _ => UnexpectedField::Unknown { + field_type: helper.field_type, + value: helper.value, + }, + } + }; + } - let helper: EditableFieldHelper = EditableFieldHelper::deserialize(deserializer)?; + let value = match helper.field_type { + FieldType::String => UnexpectedField::String(EditableFieldString(helper.value)), + FieldType::ConcealedString => { + UnexpectedField::ConcealedString(EditableFieldConcealedString(helper.value)) + } + FieldType::Boolean => deserialize_from_str!(EditableFieldBoolean => Boolean), + FieldType::Date => deserialize_from_str!(EditableFieldDate => Date), + FieldType::YearMonth => deserialize_from_str!(EditableFieldYearMonth => YearMonth), + FieldType::WifiNetworkSecurityType => { + deserialize_from_str!(EditableFieldWifiNetworkSecurityType => WifiNetworkSecurityType) + } + FieldType::SubdivisionCode => { + UnexpectedField::SubdivisionCode(EditableFieldSubdivisionCode(helper.value)) + } + FieldType::CountryCode => { + UnexpectedField::CountryCode(EditableFieldCountryCode(helper.value)) + } + FieldType::Email => UnexpectedField::Email(EditableFieldEmail(helper.value)), + FieldType::Number => UnexpectedField::Number(EditableFieldNumber(helper.value)), + FieldType::Unknown(_) => UnexpectedField::Unknown { + field_type: helper.field_type, + value: helper.value, + }, + }; - if helper.field_type != helper.value.field_type() { - return Err(serde::de::Error::custom( - "field_type does not match value type", - )); - } + ExpectedInner::Unexpected(value) + }; Ok(Self { id: helper.id, - value: helper.value, + value: Expected(value), label: helper.label, extensions: helper.extensions, }) @@ -158,7 +366,7 @@ impl From for EditableField { fn from(s: T) -> Self { EditableField { id: None, - value: s, + value: s.into(), label: None, extensions: None, } @@ -174,12 +382,20 @@ macro_rules! editable_field_string_type { pub struct $name(pub String); impl EditableFieldType for $name { - fn field_type(&self) -> FieldType { + fn field_type() -> FieldType { FieldType::$variant } } + impl From for $name { + #[inline] + fn from(s: String) -> Self { + $name(s) + } + } + impl From for EditableField<$name, E> { + #[inline] fn from(s: String) -> Self { $name(s).into() } @@ -187,7 +403,24 @@ macro_rules! editable_field_string_type { impl From> for String { fn from(s: EditableField<$name, E>) -> Self { - s.value.0 + match s.value.0 { + ExpectedInner::Expected(f) => f.0.into(), + ExpectedInner::Unexpected(f) => f.into(), + } + } + } + + impl From<$name> for String { + #[inline] + fn from(v: $name) -> Self { + v.0 + } + } + + impl AsRef for $name { + #[inline] + fn as_ref(&self) -> &str { + &self.0 } } }; @@ -200,10 +433,11 @@ editable_field_string_type!(EditableFieldNumber, Number); editable_field_string_type!(EditableFieldSubdivisionCode, SubdivisionCode); editable_field_string_type!(EditableFieldCountryCode, CountryCode); -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(transparent)] pub struct EditableFieldBoolean(#[serde(with = "serde_bool")] pub bool); impl EditableFieldType for EditableFieldBoolean { - fn field_type(&self) -> FieldType { + fn field_type() -> FieldType { FieldType::Boolean } } @@ -214,9 +448,10 @@ impl From for EditableField { } } -impl From> for bool { - fn from(b: EditableField) -> Self { - b.value.0 +impl From for String { + #[inline] + fn from(value: EditableFieldBoolean) -> Self { + if value.0 { "true" } else { "false" }.into() } } @@ -224,34 +459,37 @@ impl From> for bool { #[serde(transparent)] pub struct EditableFieldDate(pub NaiveDate); impl EditableFieldType for EditableFieldDate { - fn field_type(&self) -> FieldType { + fn field_type() -> FieldType { FieldType::Date } } -#[derive(Clone, Debug, PartialEq, Eq)] +impl From for String { + #[inline] + fn from(value: EditableFieldDate) -> Self { + value.0.format("%Y-%m-%d").to_string() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(into = "String")] pub struct EditableFieldYearMonth { /// The year in the format `YYYY` pub year: u16, /// The month in the format `MM` pub month: Month, } -impl EditableFieldType for EditableFieldYearMonth { - fn field_type(&self) -> FieldType { - FieldType::YearMonth + +impl From for String { + #[inline] + fn from(value: EditableFieldYearMonth) -> String { + format!("{:04}-{:02}", value.year, value.month.number_from_month()) } } -impl Serialize for EditableFieldYearMonth { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&format!( - "{:04}-{:02}", - self.year, - self.month.number_from_month() - )) +impl EditableFieldType for EditableFieldYearMonth { + fn field_type() -> FieldType { + FieldType::YearMonth } } @@ -323,13 +561,27 @@ pub enum EditableFieldWifiNetworkSecurityType { Other(String), } impl EditableFieldType for EditableFieldWifiNetworkSecurityType { - fn field_type(&self) -> FieldType { + fn field_type() -> FieldType { FieldType::WifiNetworkSecurityType } } +impl From for String { + #[inline] + fn from(value: EditableFieldWifiNetworkSecurityType) -> Self { + match value { + EditableFieldWifiNetworkSecurityType::Unsecured => "unsecured".into(), + EditableFieldWifiNetworkSecurityType::WpaPersonal => "wpa-personal".into(), + EditableFieldWifiNetworkSecurityType::Wpa2Personal => "wpa2-personal".into(), + EditableFieldWifiNetworkSecurityType::Wpa3Personal => "wpa3-personal".into(), + EditableFieldWifiNetworkSecurityType::Wep => "wep".into(), + EditableFieldWifiNetworkSecurityType::Other(o) => o, + } + } +} + /// Helper wrapper for `CustomFieldsCredential`. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize)] #[serde(untagged, bound(deserialize = "E: Deserialize<'de>"))] #[non_exhaustive] pub enum EditableFieldValue { @@ -345,9 +597,58 @@ pub enum EditableFieldValue { WifiNetworkSecurityType(EditableField), } -mod serde_bool { - use serde::Deserialize; +impl<'de, E> Deserialize<'de> for EditableFieldValue +where + E: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let helper: EditableFieldHelper = EditableFieldHelper::deserialize(deserializer)?; + macro_rules! deserialize_field { + ($t: tt) => {{ + let v = $t::deserialize(StringDeserializer::new(helper.value))?; + + EditableField { + id: helper.id, + value: Expected(ExpectedInner::Expected(v)), + label: helper.label, + extensions: helper.extensions, + } + }}; + } + + let res = match helper.field_type { + FieldType::String => Self::String(deserialize_field!(EditableFieldString)), + FieldType::ConcealedString => { + Self::ConcealedString(deserialize_field!(EditableFieldConcealedString)) + } + FieldType::Boolean => Self::Boolean(deserialize_field!(EditableFieldBoolean)), + FieldType::Date => Self::Date(deserialize_field!(EditableFieldDate)), + FieldType::YearMonth => Self::YearMonth(deserialize_field!(EditableFieldYearMonth)), + FieldType::WifiNetworkSecurityType => Self::WifiNetworkSecurityType( + deserialize_field!(EditableFieldWifiNetworkSecurityType), + ), + FieldType::SubdivisionCode => { + Self::SubdivisionCode(deserialize_field!(EditableFieldSubdivisionCode)) + } + FieldType::CountryCode => { + Self::CountryCode(deserialize_field!(EditableFieldCountryCode)) + } + FieldType::Email => Self::Email(deserialize_field!(EditableFieldEmail)), + FieldType::Number => Self::Number(deserialize_field!(EditableFieldNumber)), + FieldType::Unknown(_) => { + return Err(serde::de::Error::custom("Unknown custom field type")) + } + }; + + Ok(res) + } +} + +mod serde_bool { pub fn serialize(value: &bool, serializer: S) -> Result where S: serde::Serializer, @@ -359,10 +660,9 @@ mod serde_bool { where D: serde::Deserializer<'de>, { - let value = <&str>::deserialize(deserializer)?; + let s = deserializer.deserialize_str(super::CowVisitor)?; - value - .trim() + s.trim() .to_lowercase() .parse() .map_err(serde::de::Error::custom) @@ -379,7 +679,7 @@ mod tests { fn test_serialize_editable_field_string() { let field: EditableField = EditableField { id: None, - value: EditableFieldString("value".to_string()), + value: EditableFieldString("value".to_string()).into(), label: Some("label".to_string()), extensions: None, }; @@ -404,7 +704,7 @@ mod tests { field, EditableField { id: None, - value: EditableFieldString("value".to_string()), + value: EditableFieldString("value".to_string()).into(), label: Some("label".to_string()), extensions: None, } @@ -415,7 +715,7 @@ mod tests { fn test_serialize_field_concealed_string() { let field: EditableField = EditableField { id: None, - value: EditableFieldConcealedString("value".to_string()), + value: EditableFieldConcealedString("value".to_string()).into(), label: Some("label".to_string()), extensions: None, }; @@ -437,7 +737,7 @@ mod tests { let field: Result, _> = serde_json::from_value(json); - assert!(field.is_err()); + assert!(field.unwrap().value.as_expected().is_err()); } #[test] @@ -461,7 +761,7 @@ mod tests { }); let field: Result, _> = serde_json::from_value(json); - assert!(field.is_err()); + assert!(field.unwrap().value.as_expected().is_err()); } #[test] @@ -490,7 +790,7 @@ mod tests { field, EditableField { id: None, - value: EditableFieldConcealedString("value".to_string()), + value: EditableFieldConcealedString("value".to_string()).into(), label: Some("label".to_string()), extensions: None, } @@ -501,7 +801,7 @@ mod tests { fn test_serialize_field_boolean() { let field: EditableField = EditableField { id: None, - value: EditableFieldBoolean(true), + value: EditableFieldBoolean(true).into(), label: Some("label".to_string()), extensions: None, }; @@ -517,7 +817,7 @@ mod tests { fn test_serialize_field_date() { let field: EditableField = EditableField { id: None, - value: EditableFieldDate(NaiveDate::from_ymd_opt(2025, 2, 24).unwrap()), + value: EditableFieldDate(NaiveDate::from_ymd_opt(2025, 2, 24).unwrap()).into(), label: None, extensions: None, }; @@ -535,7 +835,8 @@ mod tests { value: EditableFieldYearMonth { year: 2025, month: Month::February, - }, + } + .into(), label: None, extensions: None, }; @@ -561,7 +862,8 @@ mod tests { value: EditableFieldYearMonth { year: 2025, month: Month::February, - }, + } + .into(), label: None, extensions: None, } @@ -574,8 +876,14 @@ mod tests { "fieldType": "year-month", "value": "2025/02", }); - let field: Result, _> = serde_json::from_value(json); - assert!(field.is_err()); + let field: EditableField = serde_json::from_value(json).unwrap(); + assert_eq!( + field.value.as_expected(), + Err(&UnexpectedField::Unknown { + field_type: FieldType::YearMonth, + value: "2025/02".into(), + }) + ); } #[test] @@ -603,7 +911,7 @@ mod tests { field, EditableField { id: None, - value: EditableFieldString("hello".to_string()), + value: EditableFieldString("hello".to_string()).into(), label: None, extensions: Some(vec![Extension::External(CustomExtension::Test { contents: "world".into() @@ -615,4 +923,38 @@ mod tests { assert_eq!(returned, json); } + + #[test] + fn editable_string_deserialized_from_others() { + for (field_type, expected) in [ + ( + "concealed-string", + UnexpectedField::ConcealedString("hello".to_owned().into()), + ), + ( + "unknown", + UnexpectedField::Unknown { + field_type: FieldType::Unknown("unknown".into()), + value: "hello".into(), + }, + ), + ( + "date", + UnexpectedField::Unknown { + field_type: FieldType::Date, + value: "hello".into(), + }, + ), + ] { + let json = json!({ + "fieldType": field_type, + "value": "hello", + }); + + let field: EditableField = + serde_json::from_value(json.clone()).expect("Could not deserialize field"); + + assert_eq!(field.value.into_expected(), Err(expected)); + } + } } diff --git a/credential-exchange-format/src/lib.rs b/credential-exchange-format/src/lib.rs index d98d879..44db6e2 100644 --- a/credential-exchange-format/src/lib.rs +++ b/credential-exchange-format/src/lib.rs @@ -213,11 +213,11 @@ mod tests { #[test] fn deserializes_unknown_credential() { let cred = r#"{ "type": "future-credential" }"#; - let res = serde_json::from_str::(&cred); + let res = serde_json::from_str::(cred); assert!(matches!(res, Ok(Credential::Unknown { .. }))); let empty = r#"{ }"#; - let res = serde_json::from_str::(&empty); + let res = serde_json::from_str::(empty); assert!(res.is_err()); } }