Repository: poem-web/poem (language: rust)

Issue:
Add support for serialize_with and deserialize_with
## Description of the feature

I'm currently evaluating a migration of an existing web project over to poem/poem-openapi and I was wondering if there is an equivalent to serialize_with/deserialize_with for poem_openapi::Object

I've got a project library that uses an external type in one of its fields [Geozero](https://github.com/georust/geozero)

## Code example (if possible)

```rust

pub mod MyDatabaseModels {
  use poem_openapi::Object;
  use serde::{Deserialize, Serialize};
  use serde_json;
  use geozero::wkb::Ewkb;

  #[derive(Debug, Serialize, Deserialize, Object)
  pub struct MyLargeTableA {
    pub id: uuid::Uuid,
    pub name: String,
    #[serde(
        serialize_with = "geojson_serialize",
        deserialize_with = "geojson_deserialize",
    )]
    pub geojson: Option<Ewkb>,
    // dozens of additional fields after
  };
}

```

I added #[derive(Object)] as above to MyLargeTableA but got four errors:

- the trait `Type` is not implemented for `geozero::wkb::Ewkb`
- variant or associated item cannot be called on `std::option::Option<geozero::wkb::Ewkb>` due to unsatisfied trait bounds
- the trait `ParseFromJSON` is not implemented for `geozero::wkb::Ewkb`
- the trait `ToJSON` is not implemented for `geozero::wkb::Ewkb`

I'm relatively new to rust unfortunately, but from what I understand the serde serialize_with/deserialize_with is what I'm looking for with #[oai]

Is there a better/cleaner solution?


Discussion / hints:
Hello? Any response to this?
I value what @gibbz00 implemented in #693, though interestingly the mentioned [GeometryCollection](https://datatracker.ietf.org/doc/html/rfc7946#appendix-A.7) is actually the specific implementation I am looking to store in my ewkb field.

I would prefer a more generic solution like what serde did with serialize_with/deserialize_with, but I certainly won't look a gift horse in the mouth.

Fix patch:
diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs
index f18a487057..d3ff889a7c 100644
--- a/poem-openapi-derive/src/object.rs
+++ b/poem-openapi-derive/src/object.rs
@@ -42,6 +42,10 @@ struct ObjectField {
     skip_serializing_if_is_empty: bool,
     #[darling(default)]
     skip_serializing_if: Option<Path>,
+    #[darling(default)]
+    serialize_with: Option<Path>,
+    #[darling(default)]
+    deserialize_with: Option<Path>,
 }
 
 #[derive(FromDeriveInput)]
@@ -194,15 +198,22 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
                         };
                     });
                 }
-                None => deserialize_fields.push(quote! {
-                    #[allow(non_snake_case)]
-                    let #field_ident: #field_ty = {
-                        let value = #crate_name::types::ParseFromJSON::parse_from_json(obj.remove(#field_name))
-                            .map_err(#crate_name::types::ParseError::propagate)?;
-                        #validators_checker
-                        value
+                None => {
+                    let deserialize_function = match field.deserialize_with {
+                        Some(ref function) => quote! { #function },
+                        None => quote! { #crate_name::types::ParseFromJSON::parse_from_json },
                     };
-                }),
+
+                    deserialize_fields.push(quote! {
+                        #[allow(non_snake_case)]
+                        let #field_ident: #field_ty = {
+                            let value = #deserialize_function(obj.remove(#field_name))
+                                .map_err(#crate_name::types::ParseError::propagate)?;
+                            #validators_checker
+                            value
+                        };
+                    })
+                }
             }
         } else {
             if args.deny_unknown_fields {
@@ -239,9 +250,14 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
                     quote!(true)
                 };
 
+                let serialize_function = match field.serialize_with {
+                    Some(ref function) => quote! { #function },
+                    None => quote! { #crate_name::types::ToJSON::to_json },
+                };
+
                 serialize_fields.push(quote! {
                     if #check_is_none && #check_is_empty && #check_if {
-                        if let ::std::option::Option::Some(value) = #crate_name::types::ToJSON::to_json(&self.#field_ident) {
+                        if let ::std::option::Option::Some(value) = #serialize_function(&self.#field_ident) {
                             object.insert(::std::string::ToString::to_string(#field_name), value);
                         }
                     }


Test patch:
diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs
index e70aca4f32..1aabe737e0 100644
--- a/poem-openapi/tests/object.rs
+++ b/poem-openapi/tests/object.rs
@@ -1019,3 +1019,61 @@ fn object_default_override_by_field() {
         }
     );
 }
+
+// NOTE(Rennorb): The `serialize_with` and `deserialize_with` attributes don't add any additional validation,
+// it's up to the library consumer to use them in ways were they don't violate the OpenAPI specification of the underlying type.
+//
+// In practice `serialize_with` only exists for the rounding case below, which could not be implemented in a different way before this
+// (only by using a larger type), and `deserialize_with` just exists for parity.
+
+#[test]
+fn serialize_with() {
+    #[derive(Debug, Object)]
+    struct Obj {
+        #[oai(serialize_with = "round")]
+        a: f32,
+        b: f32,
+    }
+
+    // NOTE(Rennorb): Function signature in complice with `to_json` in the Type system.
+    // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now.
+    fn round(v: &f32) -> Option<serde_json::Value> {
+        Some(serde_json::Value::from((*v as f64 * 1e5).round() / 1e5))
+    }
+
+    let obj = Obj { a: 0.3, b: 0.3 };
+
+    assert_eq!(obj.to_json(), Some(json!({"a": 0.3f64, "b": 0.3f32})));
+}
+
+#[test]
+fn deserialize_with() {
+    #[derive(Debug, PartialEq, Object)]
+    struct Obj {
+        #[oai(deserialize_with = "add")]
+        a: i32,
+    }
+
+    // NOTE(Rennorb): Function signature in complice with `parse_from_json` in the Type system.
+    // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now.
+    fn add(value: Option<serde_json::Value>) -> poem_openapi::types::ParseResult<i32> {
+        value
+            .as_ref()
+            .and_then(|v| v.as_str())
+            .and_then(|s| s.split_once('+'))
+            .and_then(|(a, b)| {
+                let parse_a = a.trim().parse::<i32>();
+                let parse_b = b.trim().parse::<i32>();
+                match (parse_a, parse_b) {
+                    (Ok(int_a), Ok(int_b)) => Some(int_a + int_b),
+                    _ => None,
+                }
+            })
+            .ok_or(poem_openapi::types::ParseError::custom("Unknown error")) // bad error, but its good enough for tests
+    }
+
+    assert_eq!(
+        Obj::parse_from_json(Some(json!({"a": "3 + 4"}))).unwrap(),
+        Obj { a: 7 }
+    );
+}