Skip to content
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

Add serde_with #198

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Add serde_with #198

wants to merge 1 commit into from

Conversation

ModProg
Copy link

@ModProg ModProg commented Jan 17, 2023

No description provided.

run: cargo update -p serde --precise 1.0.142 && cargo update -p once_cell --precise 1.10.0 && cargo update -p pretty_assertions --precise 1.2.1
run: sed -i '/serde_with =/d' schemars/Cargo.toml && cargo update -p serde --precise 1.0.142 && cargo update -p once_cell --precise 1.10.0 && cargo update -p pretty_assertions --precise 1.2.1
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while this makes ci green, it's not really feasible on release.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT with serde_with even as an optional dependency we bump MSRV to 1.60

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest version supported below is 1.14

@ModProg
Copy link
Author

ModProg commented Jan 17, 2023

One thing worth considering is adding SerdeWith adapters with a PhantomType to make it easy to define schemas:
E.g. for the Same type as that isn't able to resolve T during schema generation

pub struct Same<T>(PhantomData<T>);
impl<'de, _T, T: Deserialize<'de>> DeserializeAs<'de, T> for Same<_T> {
    fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<T, D::Error> {
        T::deserialize(deserializer)
    }
}
impl<'de, _T, T: Serialize> SerializeAs<T> for Same<_T> {
    fn serialize_as<S: Serializer>(it: &T, serializer: S) -> Result<S::Ok, S::Error> {
        it.serialize(serializer)
    }
}

impl<T: JsonSchema> JsonSchema for Same<T> {
    fn schema_name() -> String {
        T::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        T::json_schema(gen)
    }

    fn is_referenceable() -> bool {
        T::is_referenceable()
    }

    fn _schemars_private_non_optional_json_schema(
        gen: &mut schemars::gen::SchemaGenerator,
    ) -> schemars::schema::Schema {
        T::_schemars_private_non_optional_json_schema(gen)
    }

    fn _schemars_private_is_option() -> bool {
        T::_schemars_private_is_option()
    }
}

@jonasbb
Copy link

jonasbb commented Mar 1, 2023

Hi, interesting PR. From the last comment, it seems there are some issues due to type inference. I'm not familiar with schemars, so maybe my conclusion is nonsense.

serde_with quite heavily relies on inference for the whole serde_as stuff. Trying to put a schema system on top, without providing for the inference information will likely not go over well, as the previous post shows with Same. To really model the flexibility of serde(with = "") I think you might need a subschema_for function with two generic arguments: one for the field and one for the content of the attribute. Possibly something to consider for #112.

Without that option in schemars, you can fake the type inference, with another type. This seems to work. The schema looks ok to me.

pub struct Schema<T: ?Sized, TAs>(PhantomData<T>, PhantomData<TAs>);

impl<T> schemars::JsonSchema for Schema<T, Same>
where
    T: schemars::JsonSchema,
{
    fn schema_name() -> alloc::string::String {
        T::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        T::json_schema(gen)
    }
}

impl<T> schemars::JsonSchema for Schema<T, DisplayFromStr> {
    fn schema_name() -> alloc::string::String {
        alloc::string::String::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        alloc::string::String::json_schema(gen)
    }
}

impl<T> schemars::JsonSchema for Schema<Option<T>, NoneAsEmptyString> {
    fn schema_name() -> alloc::string::String {
        <Option<alloc::string::String>>::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        <Option<alloc::string::String>>::json_schema(gen)
    }
}

impl<T, TAs> schemars::JsonSchema for Schema<alloc::boxed::Box<T>, alloc::boxed::Box<TAs>>
where
    Schema<T, TAs>: schemars::JsonSchema,
{
    fn schema_name() -> alloc::string::String {
        <alloc::boxed::Box<Schema<T, TAs>>>::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        <alloc::boxed::Box<Schema<T, TAs>>>::json_schema(gen)
    }
}

impl<T, TAs> schemars::JsonSchema for Schema<alloc::vec::Vec<T>, alloc::vec::Vec<TAs>>
where
    Schema<T, TAs>: schemars::JsonSchema,
{
    fn schema_name() -> alloc::string::String {
        <alloc::vec::Vec<Schema<T, TAs>>>::schema_name()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        <alloc::vec::Vec<Schema<T, TAs>>>::json_schema(gen)
    }
}

#[test]
fn schemas() {
    use alloc::{borrow::ToOwned, boxed::Box, string::String, vec::Vec};

    #[derive(schemars::JsonSchema)]
    struct A {
        a: u32,
        #[schemars(with = "Schema::<u32, DisplayFromStr>")]
        b: u32,
        #[schemars(with = "Schema::<Box<u32>, Box<DisplayFromStr>>")]
        c: Box<u32>,
        #[schemars(with = "Schema::<Box<u32>, Box<Same>>")]
        d: Box<u32>,
        #[schemars(with = "Schema::<Vec<u32>, Vec<DisplayFromStr>>")]
        e: Vec<u32>,
        #[schemars(with = "Schema::<Option<String>, NoneAsEmptyString>")]
        f: Option<String>,
    }

    let schema = schemars::schema_for!(A);
    expect_test::expect![[r##"
        {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "title": "A",
          "type": "object",
          "required": [
            "a",
            "b",
            "c",
            "d",
            "e",
            "f"
          ],
          "properties": {
            "a": {
              "type": "integer",
              "format": "uint32",
              "minimum": 0.0
            },
            "b": {
              "$ref": "#/definitions/String"
            },
            "c": {
              "$ref": "#/definitions/String"
            },
            "d": {
              "$ref": "#/definitions/uint32"
            },
            "e": {
              "$ref": "#/definitions/Array_of_String"
            },
            "f": {
              "$ref": "#/definitions/Nullable_String"
            }
          },
          "definitions": {
            "Array_of_String": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/String"
              }
            },
            "Nullable_String": {
              "type": [
                "string",
                "null"
              ]
            },
            "String": {
              "type": "string"
            },
            "uint32": {
              "type": "integer",
              "format": "uint32",
              "minimum": 0.0
            }
          }
        }"##]]
    .assert_eq(&serde_json::to_string_pretty(&schema).unwrap());
}

If you want to go that way the question is how to generate the #[schemars(with = "Schema::<u32, DisplayFromStr>")] attributes. The schemars::JsonSchema derive could look at the serde_as or serde attributes for that. You could also consider changing the serde_with::serde_as macro to emit them. Maybe something like this.

#[serde_with::serde_as(schema = true)]
#[derive(serde::Serialize, schemars::JsonSchema)]
struct Foo {...}

Some implementations in this PR seem slightly wrong. Somebody with better schema knowledge might want to double check them. For example, BoolFromInt does not behave like u8, it will only accept 0 or 1, nothing else. DefaultOnNull is more like Option<T> since it accept none/null. StringWithSeparator is like a String, not like a T.

@ModProg
Copy link
Author

ModProg commented Mar 2, 2023

Some implementations in this PR seem slightly wrong. Somebody with better schema knowledge might want to double check them. For example, BoolFromInt does not behave like u8, it will only accept 0 or 1, nothing else. DefaultOnNull is more like Option<T> since it accept none/null. StringWithSeparator is like a String, not like a T.

sound reasonable, if this pr is considered, I'll update these and have a look at the rest.

@Kixiron
Copy link

Kixiron commented Apr 13, 2023

Any updates on this PR, I'm currently blocked on it

@jonasbb
Copy link

jonasbb commented Apr 13, 2023

@Kixiron This PR has some issues with type inference (relatively important for serde_with). Above I outlined an alternative which could also be submitted to serde_with.

@ModProg
Copy link
Author

ModProg commented Apr 14, 2023

If I get a ok by @GREsau that this would be considered I'd try incorporating the feedback above. But feel free to ask on serde_with what their position on this is.

Maybe at some point forking could also make sense.

@jonasbb
Copy link

jonasbb commented Jul 26, 2023

Since there appears to be some interest in better integrating schemars and serde_with, I threw together this PoC jonasbb/serde_with#623 based on my previous comment. I am happy to review contributions for that, but cannot build it myself as I lack the knowledge about JSON schemas and this crate. I think the PR is enough of a foundation that extending it is possible without much serde_with knowledge.

@swlynch99
Copy link

I've been working on getting this integrated over on the serde_with side.

For those interested in following along some basic support got added in jonasbb/serde_with#666 and more is currently being added in jonasbb/serde_with#676.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants