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

Not required over null #344

Open
robotmayo opened this issue Sep 25, 2024 · 1 comment
Open

Not required over null #344

robotmayo opened this issue Sep 25, 2024 · 1 comment

Comments

@robotmayo
Copy link

robotmayo commented Sep 25, 2024

Is it possible to set a value as not required? Using an optional sets the value as nullable with a validation of "allOf". I just want the value to be optional not nullable. I would expect this to be the case when option_nullable is set and option_add_null_type is not set as in the openapi3 function. This does not seem to be the case.

@robotmayo robotmayo changed the title Avoid using "anyOf/allOf/oneOf" with nullable enum properties Not required over null Sep 25, 2024
@dnlsndr
Copy link

dnlsndr commented Dec 5, 2024

I have exactly the same issue. Right now you can't actually set that toggle via the JsonSchema implementation for that type. One has to use the #[schemars(skip_serializing_if...)] attribute, which will then skip the default value for that type and make it optional.
In my case I'm implementing a "Maybe" enum, which can either be optional (undefined), null, or have a value. My code looks something like this:

#[derive(Debug, Default, Clone, Eq, PartialEq, EnumIs)]
pub enum Maybe<T> {
    #[default]
    Missing,
    Null,
    Value(T),
}

impl<T> JsonSchema for Maybe<T>
where
    T: JsonSchema,
{
    fn schema_name() -> String {
        format!("Maybe_{}", T::schema_name())
    }

    fn schema_id() -> Cow<'static, str> {
        Cow::Owned(format!("Maybe<{}>", T::schema_id()))
    }

    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
        let mut schema = gen.subschema_for::<T>();

        if gen.settings().option_add_null_type {
            schema = match schema {
                Schema::Bool(true) => Schema::Bool(true),
                Schema::Bool(false) => <()>::json_schema(gen),
                Schema::Object(SchemaObject {
                    instance_type: Some(ref mut instance_type),
                    ..
                }) => {
                    add_null_type(instance_type);
                    schema
                }
                schema => SchemaObject {
                    // TODO technically the schema already accepts null, so this may be unnecessary
                    subschemas: Some(Box::new(SubschemaValidation {
                        any_of: Some(vec![schema, <()>::json_schema(gen)]),
                        ..Default::default()
                    })),
                    ..Default::default()
                }
                .into(),
            }
        }
        if gen.settings().option_nullable {
            let mut schema_obj = schema.into_object();
            schema_obj
                .extensions
                .insert("nullable".to_owned(), json!(true));
            schema = Schema::Object(schema_obj);
        };
        schema
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use schemars::schema_for;

    #[derive(Debug, Serialize, Deserialize, Default, JsonSchema)]
    #[serde(default)]
    struct TestStruct {
        check: bool,
        a: Maybe<String>,
        b: Maybe<u32>,
        #[schemars(skip_serializing_if = "Maybe::is_missing")]
        c: Maybe<u32>,
    }

    #[test]
    fn it_generates_the_correct_jsonschema() {
        let schema = schema_for!(TestStruct);

        let serialized = serde_json::to_string_pretty(&schema).unwrap();

        println!("{}", serialized);
    }
}

Which results in this schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "TestStruct",
  "type": "object",
  "properties": {
    "a": {
      "default": null,
      "allOf": [
        {
          "$ref": "#/definitions/Maybe_String"
        }
      ]
    },
    "b": {
      "default": null,
      "allOf": [
        {
          "$ref": "#/definitions/Maybe_uint32"
        }
      ]
    },
    "c": {
      "$ref": "#/definitions/Maybe_uint32"
    },
    "check": {
      "default": false,
      "type": "boolean"
    }
  },
  "definitions": {
    "Maybe_String": {
      "type": [
        "string",
        "null"
      ]
    },
    "Maybe_uint32": {
      "type": [
        "integer",
        "null"
      ],
      "format": "uint32",
      "minimum": 0.0
    }
  }
}

Note

The c field does not have the default: null value, due to the #[schemars(skip_serializing_if...)] attribue, and it also
only just works because the structu also has a #[serde(default)] attribute

This is the only way I have found, that allows me to make a field in a struct truly optional. There's no way I can set the default value in my JsonSchema trait implementation.

JsonSchema derive result for the TestStruct

// Recursive expansion of JsonSchema macro
// ========================================

const _: () = {
    #[automatically_derived]
    #[allow(unused_braces)]
    impl schemars::JsonSchema for TestStruct {
        fn schema_name() -> std::string::String {
            "TestStruct".to_owned()
        }
        fn schema_id() -> std::borrow::Cow<'static, str> {
            std::borrow::Cow::Borrowed(std::concat!(std::module_path!(), "::", "TestStruct"))
        }
        fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
            {
                let container_default = Self::default();
                let mut schema_object = schemars::schema::SchemaObject {
                    instance_type: Some(schemars::schema::InstanceType::Object.into()),
                    ..Default::default()
                };
                let object_validation = schema_object.object();
                {
                    schemars::_private::insert_object_property::<bool>(
                        object_validation,
                        "check",
                        true,
                        false,
                        schemars::_private::metadata::add_default(
                            gen.subschema_for::<bool>(),
                            Some(container_default.check)
                                .and_then(|d| schemars::_schemars_maybe_to_value!(d)),
                        ),
                    );
                }
                {
                    schemars::_private::insert_object_property::<Maybe<String>>(
                        object_validation,
                        "a",
                        true,
                        false,
                        schemars::_private::metadata::add_default(
                            gen.subschema_for::<Maybe<String>>(),
                            Some(container_default.a)
                                .and_then(|d| schemars::_schemars_maybe_to_value!(d)),
                        ),
                    );
                }
                {
                    schemars::_private::insert_object_property::<Maybe<u32>>(
                        object_validation,
                        "b",
                        true,
                        false,
                        schemars::_private::metadata::add_default(
                            gen.subschema_for::<Maybe<u32>>(),
                            Some(container_default.b)
                                .and_then(|d| schemars::_schemars_maybe_to_value!(d)),
                        ),
                    );
                }
                {
                    schemars::_private::insert_object_property::<Maybe<u32>>(
                        object_validation,
                        "c",
                        true,
                        false,
                        schemars::_private::metadata::add_default(
                            gen.subschema_for::<Maybe<u32>>(),
                            {
                                let default = container_default.c;
                                if Maybe::is_missing(&default) {
                                    None
                                } else {
                                    Some(default)
                                }
                            }
                            .and_then(|d| schemars::_schemars_maybe_to_value!(d)),
                        ),
                    );
                }
                schemars::schema::Schema::Object(schema_object)
            }
        }
    };
};

@GREsau Do you have any recommendations or plans on allowing for more control in the JsonSchema implementation for a type, such that we can override the default value? As of now, It seems that it is only possible to modify the subschema for that type.

Looking into the code at https://github.com/GREsau/schemars/blob/v0/schemars/src/_private.rs it seems trivial to change the add_default implementation, to allow the gen.subschema_for::<Maybe<u32>>() (as can be seen in the collapsed details section for the JsonSchema derive output) to also return the preferred default value. Right now the JsonSchema derive macro forces a Some(container_default.check).and_then(|d| schemars::_schemars_maybe_to_value!(d)) meaning we can't ever specify a None for the default by ourselves via our JsonSchema type implementation.

Hope this explains the issue well. I'd be happy to help out here @GREsau as I know you're also working on the v1.0 of the library, and maybe now may still be a good point in time to introduce a breaking change such as this. Retrofitting this kind of change to the v0.8 is probably not possible, right?

Full reproduction

use schemars::JsonSchema;
use schemars::gen::SchemaGenerator;
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;
use std::borrow::Cow;

#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub enum Maybe<T> {
    #[default]
    Missing,
    Null,
    Value(T),
}

impl<T> Maybe<T> {
    pub fn is_missing(&self) -> bool {
        matches!(self, Maybe::Missing)
    }
}

impl<T> From<Option<T>> for Maybe<T> {
    #[inline]
    fn from(opt: Option<T>) -> Maybe<T> {
        match opt {
            Some(v) => Maybe::Value(v),
            None => Maybe::Null,
        }
    }
}

impl<'de, T> Deserialize<'de> for Maybe<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Option::deserialize(deserializer).map(Into::into)
    }
}

impl<T: Serialize> Serialize for Maybe<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Maybe::Value(v) => v.serialize(serializer),
            Maybe::Null | &Maybe::Missing => serializer.serialize_none(),
        }
    }
}

impl<T> JsonSchema for Maybe<T>
where
    T: JsonSchema,
{
    fn schema_name() -> String {
        format!("Maybe_{}", T::schema_name())
    }

    fn schema_id() -> Cow<'static, str> {
        Cow::Owned(format!("Maybe<{}>", T::schema_id()))
    }

    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
        let mut schema = gen.subschema_for::<T>();

        if gen.settings().option_add_null_type {
            schema = match schema {
                Schema::Bool(true) => Schema::Bool(true),
                Schema::Bool(false) => <()>::json_schema(gen),
                Schema::Object(SchemaObject {
                    instance_type: Some(ref mut instance_type),
                    ..
                }) => {
                    add_null_type(instance_type);
                    schema
                }
                schema => SchemaObject {
                    // TODO technically the schema already accepts null, so this may be unnecessary
                    subschemas: Some(Box::new(SubschemaValidation {
                        any_of: Some(vec![schema, <()>::json_schema(gen)]),
                        ..Default::default()
                    })),
                    ..Default::default()
                }
                .into(),
            }
        }
        if gen.settings().option_nullable {
            let mut schema_obj = schema.into_object();
            schema_obj
                .extensions
                .insert("nullable".to_owned(), json!(true));
            schema = Schema::Object(schema_obj);
        };
        schema
    }
}

fn add_null_type(instance_type: &mut SingleOrVec<InstanceType>) {
    match instance_type {
        SingleOrVec::Single(ty) if **ty != InstanceType::Null => {
            *instance_type = vec![**ty, InstanceType::Null].into()
        }
        SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => ty.push(InstanceType::Null),
        _ => {}
    };
}

#[cfg(test)]
mod tests {
    use super::*;
    use schemars::{JsonSchema, schema_for};
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Serialize, Deserialize, Default, JsonSchema)]
    #[serde(default)]
    struct TestStruct {
        check: bool,
        a: Maybe<String>,
        b: Maybe<u32>,
        #[schemars(skip_serializing_if = "Maybe::is_missing")]
        c: Maybe<u32>,
    }

    #[test]
    fn it_generates_the_correct_jsonschema() {
        let schema = schema_for!(TestStruct);

        let serialized = serde_json::to_string_pretty(&schema).unwrap();

        println!("{}", serialized);
    }
}

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

No branches or pull requests

2 participants