Skip to content

Commit bafe106

Browse files
authored
fix handling of null groupConfiguration (#1130)
* fix handling of null groupConfiguration Fixes #1129 . I added the "nullish" serde function and test emulating similar treatment for nullish boolean and map fields. i think the new deserialize_nullish function could supersede deserialize_nullish_boolean, deserialize_lambda_map, and possibly deserialize_lambda_dynamodb_item. but i left that out for a potential follow-up PR, to keep the blast radius small. * consolidate nullish deserializers. addressed review feedback for #1129 . all of the implementations for the removed functions were identical, so they are exactly equivalent to the generic deserialize_nullish. existing unit tests demonstrate the equivalence. functions are private to the crate and safe to remove. I added one assertion which demonstrates failing behavior for missing fields (which are *not* covered by this deserializer). the behavior for that case is the same before and after the change, it just wasn't tested. also updated field documents and the helper function to explain the semantics. * fix documentation and lint workflow errors. * test cases for events with nullish behavior Following CR feedback on #1130 . The selected cases cover booleans, dynamodb items, maps, and the GroupConfiguration data structure, so we have at least one instance of each nullish use case. I added a utility function for basic serde verification. I retrofitted it to existing tests for the data structures that got new cases, just so the code is consistent for them. There are a very large number of other similar cases that I left alone to keep the review reasonable.
1 parent 6f305bb commit bafe106

31 files changed

Lines changed: 605 additions & 203 deletions

lambda-events/src/custom_serde/mod.rs

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use serde::{
33
de::{Deserialize, Deserializer, Error as DeError},
44
ser::Serializer,
55
};
6-
use std::collections::HashMap;
76

87
#[cfg(feature = "codebuild")]
98
pub(crate) mod codebuild_time;
@@ -58,47 +57,18 @@ where
5857
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value))
5958
}
6059

61-
/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map.
62-
pub(crate) fn deserialize_lambda_map<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
60+
/// Deserializes any `Default` type, mapping JSON `null` to `T::default()`.
61+
///
62+
/// **Note** null-to-empty semantics are usually clear for container types (Map, Vec, etc).
63+
/// For most other data types, prefer modeling fields as ```Option<T>``` with #[serde(default)]
64+
/// instead of using this deserializer. Option preserves information about the message
65+
/// for the application, and default semantics for the target data type may change
66+
/// over time without warning.
67+
pub(crate) fn deserialize_nullish<'de, D, T>(deserializer: D) -> Result<T, D::Error>
6368
where
6469
D: Deserializer<'de>,
65-
K: serde::Deserialize<'de>,
66-
K: std::hash::Hash,
67-
K: std::cmp::Eq,
68-
V: serde::Deserialize<'de>,
70+
T: Default + Deserialize<'de>,
6971
{
70-
// https://github.com/serde-rs/serde/issues/1098
71-
let opt = Option::deserialize(deserializer)?;
72-
Ok(opt.unwrap_or_default())
73-
}
74-
75-
#[cfg(feature = "dynamodb")]
76-
/// Deserializes `Item`, mapping JSON `null` to an empty item.
77-
pub(crate) fn deserialize_lambda_dynamodb_item<'de, D>(deserializer: D) -> Result<serde_dynamo::Item, D::Error>
78-
where
79-
D: Deserializer<'de>,
80-
{
81-
// https://github.com/serde-rs/serde/issues/1098
82-
let opt = Option::deserialize(deserializer)?;
83-
Ok(opt.unwrap_or_default())
84-
}
85-
86-
/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map.
87-
#[cfg(any(
88-
feature = "alb",
89-
feature = "apigw",
90-
feature = "cloudwatch_events",
91-
feature = "code_commit",
92-
feature = "cognito",
93-
feature = "sns",
94-
feature = "vpc_lattice",
95-
test
96-
))]
97-
pub(crate) fn deserialize_nullish_boolean<'de, D>(deserializer: D) -> Result<bool, D::Error>
98-
where
99-
D: Deserializer<'de>,
100-
{
101-
// https://github.com/serde-rs/serde/issues/1098
10272
let opt = Option::deserialize(deserializer)?;
10373
Ok(opt.unwrap_or_default())
10474
}
@@ -107,7 +77,9 @@ where
10777
#[allow(deprecated)]
10878
mod test {
10979
use super::*;
80+
11081
use serde::{Deserialize, Serialize};
82+
use std::collections::HashMap;
11183

11284
#[test]
11385
fn test_deserialize_base64() {
@@ -141,7 +113,7 @@ mod test {
141113
fn test_deserialize_map() {
142114
#[derive(Deserialize)]
143115
struct Test {
144-
#[serde(deserialize_with = "deserialize_lambda_map")]
116+
#[serde(deserialize_with = "deserialize_nullish")]
145117
v: HashMap<String, String>,
146118
}
147119
let input = serde_json::json!({
@@ -160,9 +132,9 @@ mod test {
160132
#[cfg(feature = "dynamodb")]
161133
#[test]
162134
fn test_deserialize_lambda_dynamodb_item() {
163-
#[derive(Deserialize)]
135+
#[derive(Deserialize, Debug)]
164136
struct Test {
165-
#[serde(deserialize_with = "deserialize_lambda_dynamodb_item")]
137+
#[serde(deserialize_with = "deserialize_nullish")]
166138
v: serde_dynamo::Item,
167139
}
168140
let input = serde_json::json!({
@@ -176,13 +148,39 @@ mod test {
176148
});
177149
let decoded: Test = serde_json::from_value(input).unwrap();
178150
assert_eq!(serde_dynamo::Item::from(HashMap::new()), decoded.v);
151+
152+
let input = serde_json::json!({});
153+
let failure = serde_json::from_value::<Test>(input);
154+
assert!(failure.is_err(), "Missing field should not default: {failure:?}")
155+
}
156+
157+
#[test]
158+
fn test_deserialize_nullish() {
159+
#[derive(Debug, Default, Deserialize, PartialEq)]
160+
struct Inner {
161+
x: u32,
162+
}
163+
#[derive(Deserialize)]
164+
struct Test {
165+
#[serde(default, deserialize_with = "deserialize_nullish")]
166+
v: Inner,
167+
}
168+
169+
let decoded: Test = serde_json::from_str(r#"{"v": null}"#).unwrap();
170+
assert_eq!(decoded.v, Inner::default());
171+
172+
let decoded: Test = serde_json::from_str(r#"{}"#).unwrap();
173+
assert_eq!(decoded.v, Inner::default());
174+
175+
let decoded: Test = serde_json::from_str(r#"{"v": {"x": 42}}"#).unwrap();
176+
assert_eq!(decoded.v, Inner { x: 42 });
179177
}
180178

181179
#[test]
182180
fn test_deserialize_nullish_boolean() {
183181
#[derive(Deserialize)]
184182
struct Test {
185-
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
183+
#[serde(default, deserialize_with = "deserialize_nullish")]
186184
v: bool,
187185
}
188186

lambda-events/src/event/activemq/mod.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
55
use serde_json::Value;
66
use std::collections::HashMap;
77

8-
use crate::custom_serde::deserialize_lambda_map;
8+
use crate::custom_serde::deserialize_nullish;
99

1010
#[non_exhaustive]
1111
#[cfg_attr(feature = "builders", derive(Builder))]
@@ -54,7 +54,7 @@ pub struct ActiveMqMessage {
5454
pub data: Option<String>,
5555
pub broker_in_time: i64,
5656
pub broker_out_time: i64,
57-
#[serde(deserialize_with = "deserialize_lambda_map")]
57+
#[serde(deserialize_with = "deserialize_nullish")]
5858
#[serde(default)]
5959
pub properties: HashMap<String, String>,
6060
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
@@ -87,14 +87,24 @@ pub struct ActiveMqDestination {
8787
#[cfg(test)]
8888
mod test {
8989
use super::*;
90+
use crate::fixtures::verify_serde_roundtrip;
9091

9192
#[test]
9293
#[cfg(feature = "activemq")]
9394
fn example_activemq_event() {
94-
let data = include_bytes!("../../fixtures/example-activemq-event.json");
95-
let parsed: ActiveMqEvent = serde_json::from_slice(data).unwrap();
96-
let output: String = serde_json::to_string(&parsed).unwrap();
97-
let reparsed: ActiveMqEvent = serde_json::from_slice(output.as_bytes()).unwrap();
98-
assert_eq!(parsed, reparsed);
95+
verify_serde_roundtrip::<ActiveMqEvent>(include_bytes!("../../fixtures/example-activemq-event.json"));
96+
}
97+
98+
#[test]
99+
#[cfg(feature = "activemq")]
100+
fn example_activemq_event_null_properties() {
101+
let event: ActiveMqEvent = verify_serde_roundtrip(include_bytes!(
102+
"../../fixtures/example-activemq-event-null-properties.json"
103+
));
104+
assert_eq!(
105+
0,
106+
event.messages[0].properties.len(),
107+
"null properties should deserialize to empty map"
108+
)
99109
}
100110
}

lambda-events/src/event/alb/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
custom_serde::{
3-
deserialize_headers, deserialize_nullish_boolean, http_method, serialize_headers,
4-
serialize_multi_value_headers, serialize_query_string_parameters,
3+
deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers,
4+
serialize_query_string_parameters,
55
},
66
encodings::Body,
77
};
@@ -35,7 +35,7 @@ pub struct AlbTargetGroupRequest {
3535
#[serde(serialize_with = "serialize_multi_value_headers")]
3636
pub multi_value_headers: HeaderMap,
3737
pub request_context: AlbTargetGroupRequestContext,
38-
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
38+
#[serde(default, deserialize_with = "deserialize_nullish")]
3939
pub is_base64_encoded: bool,
4040
pub body: Option<String>,
4141
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
@@ -101,7 +101,7 @@ pub struct AlbTargetGroupResponse {
101101
pub multi_value_headers: HeaderMap,
102102
#[serde(skip_serializing_if = "Option::is_none")]
103103
pub body: Option<Body>,
104-
#[serde(default, deserialize_with = "deserialize_nullish_boolean")]
104+
#[serde(default, deserialize_with = "deserialize_nullish")]
105105
pub is_base64_encoded: bool,
106106
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
107107
/// Enabled with Cargo feature `catch-all-fields`.

0 commit comments

Comments
 (0)