Repository: not-fl3/nanoserde
Pull request #111 (2024-09-02 18:30:48)

serde_json: serialize None as null if there is default attribute
During porting of parsing lottie files to [this project](https://github.com/birhburh/macroquad_tamagotchi) from [lottie-rs](https://github.com/zimond/lottie-rs) I comparing serialized data
and nanoserde was skipping None fields with default attribute during serialization, but serde serializes them as null

not-fl3: Thanks for PR! 

I do not have a very strong opinion, but I feel like I would rather have this behavior being optional, maybe some special attribute about serializing as "null"? 
not-fl3: oh, why do I always vargue in nanoserde's PRs :facepalm: 
you are right, `null` is the way!

I just opened JSON standard and it says pretty clear that `null` is supposed to be serialized as `null` and nothing about removing fields. 

I still feel like it would be nice to have an option to skip None's in serialized json, even if they got a default value, but maybe there should be an attribute for skipping, not for adding? 
birhburh: @not-fl3, there is always #[`n?serde`(skip, default)] and this work in serde and in nanoserde also
birhburh: Checked how they skip on None in serde and of course i forgot about
`#[serde(skip_serializing_if = "Option::is_none")]`
Adding this to my nanoserde todo list
not-fl3: ` #[serde(skip_serializing_if = "Option::is_none")]` looks good, but I think it would make more sense to have it another way around: skip serializing by default, and write `null` if the attribute is set. This way we will avoid a breaking change while support all the use cases where this null is important
birhburh: Added serialize_none_as_null for struct and fields, and changed test accordingly
knickish: Thanks @birhburh, sorry was slow to review

Patch:
diff --git a/derive/src/serde_json.rs b/derive/src/serde_json.rs
index 0bedcd4..ba2ad83 100644
--- a/derive/src/serde_json.rs
+++ b/derive/src/serde_json.rs
@@ -57,16 +57,42 @@ pub fn derive_ser_json_struct(struct_: &Struct) -> TokenStream {
             let proxied_field = ser_proxy_guard(&format!("self.{struct_fieldname}"), field);
 
             if field.ty.base() == "Option" {
+                let proxy_attr = crate::shared::attrs_proxy(&field.attributes);
+                let struct_null_on_none = shared::attrs_serialize_none_as_null(&struct_.attributes);
+                let field_null_on_none = shared::attrs_serialize_none_as_null(&field.attributes);
+                let null_on_none = (field_null_on_none || struct_null_on_none) && proxy_attr.is_none();
+                let field_header = &format!("if first_field_was_serialized {{
+                                                 s.conl();
+                                             }};
+                                             first_field_was_serialized = true;
+                                             s.field(d+1, \"{}\");", json_fieldname);
                 l!(
                     s,
-                    "if let Some(t) = &{} {{ if first_field_was_serialized {{ s.conl(); }};first_field_was_serialized = true;s.field(d+1, \"{}\");t.ser_json(d+1, s);}};",
+                    "{}
+                    if let Some(t) = &{} {{
+                        {}
+                        t.ser_json(d+1, s);
+                    }} {}",
+                    if null_on_none { field_header } else { "" },
                     proxied_field,
-                    json_fieldname
+                    if null_on_none { "" } else { field_header },
+                    if null_on_none {
+                        "else {{
+                            Option::<i32>::ser_json(&None, d+1, s);
+                        }}"
+                    } else {
+                        ""
+                    }
                 );
             } else {
                 l!(
                     s,
-                    "if first_field_was_serialized {{ s.conl(); }};first_field_was_serialized = true;s.field(d+1,\"{}\"); {}.ser_json(d+1, s);",
+                    "if first_field_was_serialized {{
+                        s.conl();
+                    }};
+                    first_field_was_serialized = true;
+                    s.field(d+1,\"{}\");
+                    {}.ser_json(d+1, s);",
                     json_fieldname,
                     proxied_field
                 );
diff --git a/derive/src/shared.rs b/derive/src/shared.rs
index b826a17..62d9449 100644
--- a/derive/src/shared.rs
+++ b/derive/src/shared.rs
@@ -76,6 +76,13 @@ pub fn attrs_skip(attributes: &[crate::parse::Attribute]) -> bool {
         .any(|attr| attr.tokens.len() == 1 && attr.tokens[0] == "skip")
 }
 
+#[cfg(feature = "json")]
+pub fn attrs_serialize_none_as_null(attributes: &[crate::parse::Attribute]) -> bool {
+    attributes
+        .iter()
+        .any(|attr| attr.tokens.len() == 1 && attr.tokens[0] == "serialize_none_as_null")
+}
+
 #[cfg(any(feature = "binary", feature = "json"))]
 pub(crate) fn struct_bounds_strings(struct_: &Struct, bound_name: &str) -> (String, String) {
     let generics: &Vec<_> = &struct_.generics;


Test patch:
diff --git a/tests/json.rs b/tests/json.rs
index d8b0bc8..0f11100 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -326,6 +326,38 @@ fn de_field_default() {
     assert_eq!(test.foo2.x, 3);
 }
 
+#[test]
+fn ser_none_as_null() {
+    #[derive(SerJson)]
+    struct Foo {
+        x: Option<i32>,
+    }
+
+    let a = Foo { x: None };
+    assert_eq!(SerJson::serialize_json(&a), r#"{}"#);
+
+    #[derive(SerJson)]
+    #[nserde(serialize_none_as_null)]
+    struct Foo2 {
+        x: Option<i32>,
+    }
+
+    let b = Foo2 { x: None };
+
+    assert_eq!(SerJson::serialize_json(&b), r#"{"x":null}"#);
+
+    #[derive(SerJson)]
+    struct Foo3 {
+        x: Option<i32>,
+        #[nserde(serialize_none_as_null)]
+        y: Option<i32>,
+    }
+
+    let b = Foo3 { x: None, y: None };
+
+    assert_eq!(SerJson::serialize_json(&b), r#"{"y":null}"#);
+}
+
 #[test]
 fn de_ser_field_skip() {
     #[derive(DeJson, SerJson)]