diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ed3bff4..d37634e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,16 +7,16 @@ jobs: runs-on: ubuntu-latest env: # work-around https://github.com/rust-lang/cargo/issues/10303 - CARGO_NET_GIT_FETCH_WITH_CLI: ${{ matrix.rust == '1.45.0' }} + CARGO_NET_GIT_FETCH_WITH_CLI: ${{ matrix.rust == '1.60.0' }} strategy: matrix: rust: - - 1.45.0 + - 1.60.0 - stable - beta - nightly include: - - rust: 1.45.0 + - rust: 1.60.0 test_features: "--features impl_json_schema" allow_failure: false - rust: stable @@ -36,9 +36,6 @@ jobs: profile: minimal toolchain: ${{ matrix.rust }} override: true - - if: matrix.rust == '1.45.0' - # work-around https://github.com/serde-rs/serde/issues/2255 and similar crate/rustc compatibility issues - 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 && cargo update -p trybuild --precise 1.0.64 - name: Check with no feature flags run: cargo check --verbose --no-default-features continue-on-error: ${{ matrix.allow_failure }} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b2bfead1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.check.command": "clippy", + "rust-analyzer.showUnlinkedFileNotification": false +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f59e662..6886f578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,90 +1,155 @@ # Changelog +## [0.8.15] - 2023-09-17 + +### Added: + +- Implement `JsonSchema` for `BigDecimal` from `bigdecimal` 0.4 (https://github.com/GREsau/schemars/pull/237) + +## [0.8.14] - 2023-09-17 + +### Added: + +- Add `#[schemars(inner(...)]` attribute to specify schema for array items (https://github.com/GREsau/schemars/pull/234) + +### Changed: + +- New optional associated function on `JsonSchema` trait: `schema_id()`, which is similar to `schema_name()`, but does not have to be human-readable, and defaults to the type name including module path. This allows schemars to differentiate between types with the same name in different modules/crates (https://github.com/GREsau/schemars/issues/62 / https://github.com/GREsau/schemars/pull/247) + +### Fixed: + +- Schemas for `rust_decimal::Decimal` and `bigdecimal::BigDecimal` now match how those types are serialized by default, i.e. as numeric strings (https://github.com/GREsau/schemars/pull/248) + +## [0.8.13] - 2023-08-28 + +### Added: + +- Implement `JsonSchema` for `semver::Version` (https://github.com/GREsau/schemars/pull/195 / https://github.com/GREsau/schemars/pull/238) +- Include const generics in generated schema names (https://github.com/GREsau/schemars/pull/179 / https://github.com/GREsau/schemars/pull/239) +- Implement `JsonSchema` for types from indexmap v2 (https://github.com/GREsau/schemars/pull/226 / https://github.com/GREsau/schemars/pull/240) +- Implement `JsonSchema` for `serde_json::value::RawValue` (https://github.com/GREsau/schemars/pull/183) + +### Changed: + +- Minimum supported rust version is now 1.60.0 + ## [0.8.12] - 2023-02-26 + ### Added: + - Implement `JsonSchema` for `smol_str::SmolStr` (https://github.com/GREsau/schemars/pull/72) ### Changed: + - Change `serde_json` dependency min version to 1.0.25 (was 1.0.0) (https://github.com/GREsau/schemars/pull/192) ## [0.8.11] - 2022-10-02 + ### Added: + - Replace auto-inferred trait bounds with bounds specified in `#[schemars(bound = "...")]` attribute ### Changed: + - Derived `JsonSchema` now respects attributes on unit enum variants (https://github.com/GREsau/schemars/pull/152) - Minimum supported rust version is now 1.45.0 ## [0.8.10] - 2022-05-17 + - Undo "Support generic default values in default attributes (https://github.com/GREsau/schemars/pull/83)" as it inadvertently introduced a breaking change (https://github.com/GREsau/schemars/issues/144) ## [0.8.9] - 2022-05-16 + ### Added: + - ~~Support generic default values in `default` attributes (https://github.com/GREsau/schemars/pull/83)~~ - - ⚠️ **This inadvertently introduced a breaking change and was removed in 0.8.10** + - ⚠️ **This inadvertently introduced a breaking change and was removed in 0.8.10** - Add missing MIT licence text for usage of code from regex_syntax crate (https://github.com/GREsau/schemars/pull/132) - Support uuid v1 and arrayvec 0.7 via feature flags `uuid1` and `arrayvec07` (https://github.com/GREsau/schemars/pull/142) - - This also adds `uuid08` and `arrayvec05` feature flags for the previously supported versions of these crates. The existing `uuid` and `arrayvec` flags are still supported for backward-compatibility, but they are **deprecated**. - - Similarly, `indexmap1` feature flag is added, and `indexmap` flag is **deprecated**. + - This also adds `uuid08` and `arrayvec05` feature flags for the previously supported versions of these crates. The existing `uuid` and `arrayvec` flags are still supported for backward-compatibility, but they are **deprecated**. + - Similarly, `indexmap1` feature flag is added, and `indexmap` flag is **deprecated**. ## [0.8.8] - 2021-11-25 + ### Added: + - Implement `JsonSchema` for types from `rust_decimal` and `bigdecimal` crates (https://github.com/GREsau/schemars/pull/101) ### Fixed: + - Fixes for internally tagged enums and flattening additional_properties (https://github.com/GREsau/schemars/pull/113) ## [0.8.7] - 2021-11-14 + ### Added: + - Implement `JsonSchema` for `EnumSet` (https://github.com/GREsau/schemars/pull/92) ### Fixed: + - Do not cause compile error when using a default value that doesn't implement `Serialize` (https://github.com/GREsau/schemars/issues/115) ## [0.8.6] - 2021-09-26 + ### Changed: + - Use `oneOf` instead of `anyOf` for enums when possible (https://github.com/GREsau/schemars/issues/108) ## [0.8.5] - 2021-09-20 + ### Fixed: + - Allow fields with plain `#[validate]` attributes (https://github.com/GREsau/schemars/issues/109) ## [0.8.4] - 2021-09-19 + ### Added: + - `#[schemars(schema_with = "...")]` attribute can now be set on enum variants. - Deriving JsonSchema will now take into account `#[validate(...)]` attributes, compatible with the [validator](https://github.com/Keats/validator) crate (https://github.com/GREsau/schemars/pull/78) ## [0.8.3] - 2021-04-05 + ### Added: + - Support for `#[schemars(crate = "...")]` attribute to allow deriving JsonSchema when the schemars crate is aliased to a different name (https://github.com/GREsau/schemars/pull/55 / https://github.com/GREsau/schemars/pull/80) - Implement `JsonSchema` for `bytes::Bytes` and `bytes::BytesMut` (https://github.com/GREsau/schemars/pull/68) ### Fixed: + - Fix deriving JsonSchema on types defined inside macros (https://github.com/GREsau/schemars/issues/59 / https://github.com/GREsau/schemars/issues/66 / https://github.com/GREsau/schemars/pull/79) ## [0.8.2] - 2021-03-27 + ### Added: + - Enable generating a schema from any serializable value using `schema_for_value!(...)` macro or `SchemaGenerator::root_schema_for_value()`/`SchemaGenerator::into_root_schema_for_value()` methods (https://github.com/GREsau/schemars/pull/75) - `#[derive(JsonSchema_repr)]` can be used on C-like enums for generating a serde_repr-compatible schema (https://github.com/GREsau/schemars/pull/76) - Implement `JsonSchema` for `url::Url` (https://github.com/GREsau/schemars/pull/63) ## [0.8.1] - 2021-03-23 + ### Added: + - `SchemaGenerator::definitions_mut()` which returns a mutable reference to the generator's schema definitions - Implement `JsonSchema` for slices ### Changed: + - Minimum supported rust version is now 1.37.0 - Deriving JsonSchema on enums now sets `additionalProperties` to false on generated schemas wherever serde doesn't accept unknown properties. This includes non-unit variants of externally tagged enums, and struct-style variants of all enums that have the `deny_unknown_fields` attribute. - Schemas for HashSet and BTreeSet now have `uniqueItems` set to true (https://github.com/GREsau/schemars/pull/64) ### Fixed + - Fix use of `#[serde(transparent)]` in combination with `#[schemars(with = ...)]` (https://github.com/GREsau/schemars/pull/67) - Fix clippy `field_reassign_with_default` warning in schemars_derive generated code in rust <1.51 (https://github.com/GREsau/schemars/pull/65) - Prevent stack overflow when using `inline_subschemas` with recursive types ## [0.8.0] - 2020-09-27 + ### Added: + - `visit::Visitor`, a trait for updating a schema and all schemas it contains recursively. A `SchemaSettings` can now contain a list of visitors. - `into_object()` method added to `Schema` as a shortcut for `into::()` - Preserve order of schema properties under `preserve_order` feature flag (https://github.com/GREsau/schemars/issues/32) @@ -93,112 +158,160 @@ - `SchemaSettings::inline_subschemas` - enforces inlining of all subschemas instead of using references (https://github.com/GREsau/schemars/issues/44) ### Removed (**BREAKING CHANGES**): + - `SchemaSettings::bool_schemas` - this has been superseded by the `ReplaceBoolSchemas` visitor - `SchemaSettings::allow_ref_siblings` - this has been superseded by the `RemoveRefSiblings` visitor - `SchemaSettings` no longer implements `PartialEq` - `SchemaGenerator::into_definitions()` - this has been superseded by `SchemaGenerator::take_definitions()` ### Changed: + - **BREAKING CHANGE** Minimum supported rust version is now 1.36.0 ### Fixed: + - **BREAKING CHANGE** unknown items in `#[schemars(...)]` attributes now cause a compilation error (https://github.com/GREsau/schemars/issues/18) ### Deprecated: + - `make_extensible`, `schema_for_any`, and `schema_for_none` methods on `SchemaGenerator` ## [0.7.6] - 2020-05-17 + ### Added: + - `#[schemars(example = "...")]` attribute for setting examples on generated schemas (https://github.com/GREsau/schemars/issues/23) ## [0.7.5] - 2020-05-17 + ### Added: + - Setting `#[deprecated]` attribute will now cause generated schemas to have the `deprecated` property set to `true` - Respect `#[serde(transparent)]` attribute (https://github.com/GREsau/schemars/issues/17) - `#[schemars(title = "...", description = "...")]` can now be used to set schema title/description. If present, these values will be used instead of doc comments (https://github.com/GREsau/schemars/issues/13) ### Changed: + - schemars_derive is now an optional dependency, but included by default ## [0.7.4] - 2020-05-16 + ### Added: + - If a struct is annotated with `#[serde(deny_unknown_fields)]`, generated schema will have `additionalProperties` set to `false` (https://github.com/GREsau/schemars/pull/30) - Set `type` property to `string` on simple enums (https://github.com/GREsau/schemars/issues/28) ## [0.7.3] - 2020-05-15 + ### Added: + - `#[schemars(schema_with = "...")]` attribute can be set on variants and fields. This allows you to specify another function which returns the schema you want, which is particularly useful on fields of types that don't implement the JsonSchema trait (https://github.com/GREsau/schemars/issues/15) ### Fixed + - `#[serde(with = "...")]`/`#[schemars(with = "...")]` attributes on enum variants are now respected - Some compiler errors generated by schemars_derive should now have more accurate spans ## [0.7.2] - 2020-04-30 + ### Added: + - Enable deriving JsonSchema on adjacent tagged enums (https://github.com/GREsau/schemars/issues/4) ## [0.7.1] - 2020-04-11 + ### Added: + - Added `examples` (https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5) to `Metadata` ### Fixed + - Fixed a bug in schemars_derive causing a compile error when the `default`, `skip_serializing_if`, and `serialize_with`/`with` attributes are used together (https://github.com/GREsau/schemars/issues/26) ## [0.7.0] - 2020-03-24 + ### Changed: + - **BREAKING CHANGE** - `SchemaSettings` can no longer be created using struct initialization syntax. Instead, if you need to use custom schema settings, you can use a constructor function and either: - - assign it to a `mut` variable and modify its public fields - - call the `with(|s| ...)` method on the settings and modify the settings inside the closure/function (as in the custom_settings.rs example) + - assign it to a `mut` variable and modify its public fields + - call the `with(|s| ...)` method on the settings and modify the settings inside the closure/function (as in the custom_settings.rs example) + ### Fixed: + - When deriving `JsonSchema` on structs, `Option` struct fields are no longer included in the list of required properties in the schema (https://github.com/GREsau/schemars/issues/11) - Fix deriving `JsonSchema` when a non-std `String` type is in scope (https://github.com/GREsau/schemars/pull/19) - This will now compile: `#[schemars(with="()")]` + ### Added: + - Added `allow_ref_siblings` setting to `SchemaSettings`. When enabled, schemas with a `$ref` property may have other properties set. - Can create JSON Schema 2019-09 schemas using `SchemaSettings::draft2019_09()` (which enables `allow_ref_siblings`) ## [0.6.5] - 2019-12-29 + ### Added: + - Implemented `JsonSchema` on types from `smallvec` and `arrayvec` (as optional dependencies) ## [0.6.4] - 2019-12-27 + ### Added: + - Implemented `JsonSchema` on types from `indexmap`, `either` and `uuid` (as optional dependencies) + ### Changed + - Remove trait bounds from Map/Set JsonSchema impls. They are unnecessary as we never create/use any instances of these types. ## [0.6.3] - 2019-12-27 + - No actual code changes - this version was just published to fix broken README on crates.io ## [0.6.2] - 2019-12-27 + ### Added: + - Documentation website available at https://graham.cool/schemars/! + ### Changed: + - Rename `derive_json_schema` to `impl_json_schema`. `derive_json_schema` is still available for backward-compatibility, but will be removed in a future version. - Improve schema naming for deriving on remote types. A `#[serde(remote = "Duration")]` attribute is now treated similarly to `#[serde(rename = "Duration")]`. - Ensure root schemas do not have a `$ref` property. If necessary, wrap the `$ref` in an `allOf`. ## [0.6.1] - 2019-12-09 + ### Fixed: + - Fix a compile error that can occur when deriving `JsonSchema` from a project that doesn't reference serde_json ## [0.6.0] - 2019-12-09 + ### Added: + - When deriving `JsonSchema`, the schema's `title` and `description` are now set from `#[doc]` comments (https://github.com/GREsau/schemars/issues/7) - When deriving `JsonSchema` on structs using a `#[serde(default)]` attribute, the schema's properties will now include `default`, unless the default value is skipped by the field's `skip_serializing_if` function (https://github.com/GREsau/schemars/issues/6) + ### Changed: + - When the `option_nullable` setting is enabled (e.g. for openapi 3), schemas for `Option` will no longer inline `T`'s schema when it should be referenceable. ## [0.5.1] - 2019-10-30 + ### Fixed: + - Added missing doc comment for `title` schema property ## [0.5.0] - 2019-10-30 + ### Added: + - Implemented `JsonSchema` for more standard library types (https://github.com/GREsau/schemars/issues/3) + ### Changed: + - Unsigned integer types (usize, u8 etc.) now have their [`minimum`](https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6.2.4) explicitly set to zero - Made prepositions/conjunctions in generated schema names lowercase - - e.g. schema name for `Result>` has changed from "Result_Of_MyStruct_Or_Array_Of_String" to "Result_of_MyStruct_or_Array_of_String" + - e.g. schema name for `Result>` has changed from "Result_Of_MyStruct_Or_Array_Of_String" to "Result_of_MyStruct_or_Array_of_String" - Some provided `JsonSchema` implementations with the same `type` but different `format`s (e.g. `i8` and `usize`) used the `type` as their name. They have now been updated to use `format` as their name. - - Previously, schema generation would incorrectly assume types such as `MyStruct` and `MyStruct` were identical, and give them a single schema definition called `MyStruct_for_Integer` despite the fact they should have different schemas. Now they will each have their own schema (`MyStruct_for_i8` and `MyStruct_for_usize` respectively). + - Previously, schema generation would incorrectly assume types such as `MyStruct` and `MyStruct` were identical, and give them a single schema definition called `MyStruct_for_Integer` despite the fact they should have different schemas. Now they will each have their own schema (`MyStruct_for_i8` and `MyStruct_for_usize` respectively). diff --git a/Cargo.toml b/Cargo.toml index 05404182..075df0e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,4 @@ members = [ "schemars", "schemars_derive" ] +resolver = "2" diff --git a/README.md b/README.md index f83649b2..1b7ff86e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI Build](https://img.shields.io/github/actions/workflow/status/GREsau/schemars/ci.yml?branch=master&logo=GitHub)](https://github.com/GREsau/schemars/actions) [![Crates.io](https://img.shields.io/crates/v/schemars)](https://crates.io/crates/schemars) [![Docs](https://docs.rs/schemars/badge.svg)](https://docs.rs/schemars) -[![rustc 1.45+](https://img.shields.io/badge/schemars-rustc_1.45+-lightgray.svg)](https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html) +[![MSRV 1.60+](https://img.shields.io/badge/schemars-rustc_1.60+-lightgray.svg)](https://blog.rust-lang.org/2022/04/07/Rust-1.60.0.html) Generate JSON Schema documents from Rust code @@ -39,10 +39,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "$schema": "http://json-schema.org/draft-07/schema#", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], + "required": ["my_bool", "my_int"], "properties": { "my_bool": { "type": "boolean" @@ -67,9 +64,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "anyOf": [ { "type": "object", - "required": [ - "StringNewType" - ], + "required": ["StringNewType"], "properties": { "StringNewType": { "type": "string" @@ -79,15 +74,11 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); }, { "type": "object", - "required": [ - "StructVariant" - ], + "required": ["StructVariant"], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], + "required": ["floats"], "properties": { "floats": { "type": "array", @@ -106,11 +97,12 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } } ``` + ### Serde Compatibility -One of the main aims of this library is compatibility with [Serde](https://github.com/serde-rs/serde). Any generated schema *should* match how [serde_json](https://github.com/serde-rs/json) would serialize/deserialize to/from JSON. To support this, Schemars will check for any `#[serde(...)]` attributes on types that derive `JsonSchema`, and adjust the generated schema accordingly. +One of the main aims of this library is compatibility with [Serde](https://github.com/serde-rs/serde). Any generated schema _should_ match how [serde_json](https://github.com/serde-rs/json) would serialize/deserialize to/from JSON. To support this, Schemars will check for any `#[serde(...)]` attributes on types that derive `JsonSchema`, and adjust the generated schema accordingly. ```rust use schemars::{schema_for, JsonSchema}; @@ -145,10 +137,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "$schema": "http://json-schema.org/draft-07/schema#", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], + "required": ["myBool", "myNumber"], "properties": { "myBool": { "type": "boolean" @@ -178,9 +167,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); }, { "type": "object", - "required": [ - "floats" - ], + "required": ["floats"], "properties": { "floats": { "type": "array", @@ -196,6 +183,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } } ``` + `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde. @@ -257,16 +245,21 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } } ``` + ## Feature Flags + - `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro - `impl_json_schema` - implements `JsonSchema` for Schemars types themselves - `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject` +- `raw_value` - implements `JsonSchema` for `serde_json::value::RawValue` (enables the serde_json `raw_value` feature) Schemars can implement `JsonSchema` on types from several popular crates, enabled via feature flags (dependency versions are shown in brackets): + - `chrono` - [chrono](https://crates.io/crates/chrono) (^0.4) - `indexmap1` - [indexmap](https://crates.io/crates/indexmap) (^1.2) +- `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0) - `either` - [either](https://crates.io/crates/either) (^1.3) - `uuid08` - [uuid](https://crates.io/crates/uuid) (^0.8) - `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) @@ -277,8 +270,10 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable - `bytes` - [bytes](https://crates.io/crates/bytes) (^1.0) - `enumset` - [enumset](https://crates.io/crates/enumset) (^1.0) - `rust_decimal` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0) -- `bigdecimal` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3) +- `bigdecimal03` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3) +- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4) - `smol_str` - [smol_str](https://crates.io/crates/smol_str) (^0.1.17) +- `semver` - [semver](https://crates.io/crates/semver) (^1.0.9) - `camino` - [camino](https://crates.io/crates/camino) (^1.1) For example, to implement `JsonSchema` on types from `chrono`, enable it as a feature in the `schemars` dependency in your `Cargo.toml` like so: diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index 90bfd5f6..00000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -msrv = "1.45.0" diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index 251a6a10..2ce0b65a 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -276,6 +276,20 @@ Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rus Set the path to the schemars crate instance the generated code should depend on. This is mostly useful for other crates that depend on schemars in their macros. +

+ +`#[schemars(inner(...))]` +

+ +Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example: + +```rs +struct Struct { + #[schemars(inner(url, regex(pattern = "^https://")))] + urls: Vec, +} +``` +

Doc Comments (`#[doc = "..."]`) diff --git a/docs/2-implementing.md b/docs/2-implementing.md index a0183d9d..c8dfb6ec 100644 --- a/docs/2-implementing.md +++ b/docs/2-implementing.md @@ -10,22 +10,54 @@ permalink: /implementing/ [Deriving `JsonSchema`]({{ site.baseurl }}{% link 1-deriving.md %}) is usually the easiest way to enable JSON schema generation for your types. But if you need more customisation, you can also implement `JsonSchema` manually. This trait has two associated functions which must be implemented, and one which can optionally be implemented: ## schema_name + ```rust fn schema_name() -> String; ``` -This function returns the name of the type's schema, which frequently is just the name of the type itself. The schema name is used as the title for root schemas, and the key within the root's `definitions` property for subschemas. +This function returns the human-readable friendly name of the type's schema, which frequently is just the name of the type itself. The schema name is used as the title for root schemas, and the key within the root's `definitions` property for subschemas. + +NB in a future version of schemars, it's likely that this function will be changed to return a `Cow<'static, str>`. + +## schema_id + +```rust +fn schema_id() -> Cow<'static, str>; +``` + +This function returns a unique identifier of the type's schema - if two types return the same `schema_id`, then Schemars will consider them identical types. Because of this, if a type takes any generic type parameters, then its ID should depend on the type arguments. For example, the implementation of this function for `Vec where T: JsonSchema` is: -If two types return the same `schema_name`, then Schemars will consider them identical types. Because of this, if a type takes any generic type parameters, then its schema name should depend on the type arguments. For example, the imlementation of this function for `Vec where T: JsonSchema` is: ```rust -fn schema_name() -> String { - format!("Array_of_{}", T::schema_name()) +fn schema_id() -> Cow<'static, str> { + Cow::Owned( + format!("[{}]", T::schema_id())) } ``` -`BTreeSet`, `LinkedList`, and similar collection types also use that implementation, since they produce identical JSON schemas so they can be considered the same type. +`&mut Vec<&T>`, `LinkedList`, `Mutex>>`, and similar collection types also use that implementation, since they produce identical JSON schemas so they can be considered the same type. + +For a type with no generic type arguments, a reasonable implementation of this function would be to return the type name including module path (in case there is a type with the same name in another module/crate), e.g.: + +```rust +impl JsonSchema for NonGenericType { + fn schema_name() -> String { + // Exclude the module path to make the name in generated schemas clearer. + "NonGenericType".to_owned() + } + + fn schema_id() -> Cow<'static, str> { + // Include the module, in case a type with the same name is in another module/crate + Cow::Borrowed(concat!(module_path!(), "::NonGenericType")) + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + todo!() + } +} +``` ## json_schema + ```rust fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema; ``` @@ -35,6 +67,7 @@ This function creates the JSON schema itself. The `gen` argument can be used to `json_schema` should not return a `$ref` schema. ## is_referenceable (optional) + ```rust fn is_referenceable() -> bool; ``` @@ -43,4 +76,4 @@ If this function returns `true`, then Schemars can re-use the generate schema wh Generally, this should return `false` for types with simple schemas (such as primitives). For more complex types, it should return `true`. For recursive types, this **must** return `true` to prevent infinite cycles when generating schemas. -The default implementation of this function returns `true` to reduce the chance of someone inadvertently causing infinite cycles with recursive types. \ No newline at end of file +The default implementation of this function returns `true` to reduce the chance of someone inadvertently causing infinite cycles with recursive types. diff --git a/docs/4-features.md b/docs/4-features.md index dc3cc850..5ef68e52 100644 --- a/docs/4-features.md +++ b/docs/4-features.md @@ -6,13 +6,17 @@ permalink: /features/ --- # Feature Flags and Optional Dependencies + - `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro - `impl_json_schema` - implements `JsonSchema` for Schemars types themselves - `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject` +- `raw_value` - implements `JsonSchema` for `serde_json::value::RawValue` (enables the serde_json `raw_value` feature) Schemars can implement `JsonSchema` on types from several popular crates, enabled via feature flags (dependency versions are shown in brackets): + - `chrono` - [chrono](https://crates.io/crates/chrono) (^0.4) - `indexmap1` - [indexmap](https://crates.io/crates/indexmap) (^1.2) +- `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0) - `either` - [either](https://crates.io/crates/either) (^1.3) - `uuid08` - [uuid](https://crates.io/crates/uuid) (^0.8) - `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) @@ -23,8 +27,10 @@ Schemars can implement `JsonSchema` on types from several popular crates, enable - `bytes` - [bytes](https://crates.io/crates/bytes) (^1.0) - `enumset` - [enumset](https://crates.io/crates/enumset) (^1.0) - `rust_decimal` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0) -- `bigdecimal` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3) +- `bigdecimal03` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3) +- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4) - `smol_str` - [smol_str](https://crates.io/crates/smol_str) (^0.1.17) +- `semver` - [semver](https://crates.io/crates/semver) (^1.0.9) For example, to implement `JsonSchema` on types from `chrono`, enable it as a feature in the `schemars` dependency in your `Cargo.toml` like so: diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs index cd69b527..4ad2503f 100644 --- a/docs/_includes/examples/schemars_attrs.rs +++ b/docs/_includes/examples/schemars_attrs.rs @@ -10,6 +10,8 @@ pub struct MyStruct { pub my_bool: bool, #[schemars(default)] pub my_nullable_enum: Option, + #[schemars(inner(regex(pattern = "^x$")))] + pub my_vec_str: Vec, } #[derive(Deserialize, Serialize, JsonSchema)] diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json index 958cb6bb..9a6a22a6 100644 --- a/docs/_includes/examples/schemars_attrs.schema.json +++ b/docs/_includes/examples/schemars_attrs.schema.json @@ -4,7 +4,8 @@ "type": "object", "required": [ "myBool", - "myNumber" + "myNumber", + "myVecStr" ], "properties": { "myBool": { @@ -26,6 +27,13 @@ "format": "int32", "maximum": 10.0, "minimum": 1.0 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index 967fe00a..a8cc5a37 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -3,17 +3,18 @@ name = "schemars" description = "Generate JSON Schemas from Rust code" homepage = "https://graham.cool/schemars/" repository = "https://github.com/GREsau/schemars" -version = "0.8.12" +version = "0.8.15" authors = ["Graham Esau "] -edition = "2018" +edition = "2021" license = "MIT" readme = "README.md" keywords = ["rust", "json-schema", "serde"] categories = ["encoding"] build = "build.rs" +rust-version = "1.60" [dependencies] -schemars_derive = { version = "=0.8.12", optional = true, path = "../schemars_derive" } +schemars_derive = { version = "=0.8.15", optional = true, path = "../schemars_derive" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.25" dyn-clone = "1.0" @@ -21,6 +22,7 @@ dyn-clone = "1.0" camino = { version = "1.1", optional = true } chrono = { version = "0.4", default-features = false, optional = true } indexmap = { version = "1.2", features = ["serde-1"], optional = true } +indexmap2 = { version = "2.0", features = ["serde"], optional = true, package = "indexmap" } either = { version = "1.3", default-features = false, optional = true } uuid08 = { version = "0.8", default-features = false, optional = true, package = "uuid" } uuid1 = { version = "1.0", default-features = false, optional = true, package = "uuid" } @@ -30,9 +32,11 @@ arrayvec07 = { version = "0.7", default-features = false, optional = true, packa url = { version = "2.0", default-features = false, optional = true } bytes = { version = "1.0", optional = true } rust_decimal = { version = "1", default-features = false, optional = true } -bigdecimal = { version = "0.3", default-features = false, optional = true } +bigdecimal03 = { version = "0.3", default-features = false, optional = true, package = "bigdecimal" } +bigdecimal04 = { version = "0.4", default-features = false, optional = true, package = "bigdecimal" } enumset = { version = "1.0", optional = true } smol_str = { version = "0.1.17", optional = true } +semver = { version = "1.0.9", features = ["serde"], optional = true } [dev-dependencies] pretty_assertions = "1.2.1" @@ -58,6 +62,10 @@ uuid = ["uuid08"] arrayvec = ["arrayvec05"] indexmap1 = ["indexmap"] +raw_value = ["serde_json/raw_value"] +# `bigdecimal` feature without version suffix is included only for back-compat - will be removed in a later version +bigdecimal = ["bigdecimal03"] + ui_test = [] [[test]] @@ -68,6 +76,10 @@ required-features = ["chrono"] name = "indexmap" required-features = ["indexmap"] +[[test]] +name = "indexmap2" +required-features = ["indexmap2"] + [[test]] name = "either" required-features = ["either"] @@ -108,5 +120,13 @@ required-features = ["enumset"] name = "smol_str" required-features = ["smol_str"] +[[test]] +name = "semver" +required-features = ["semver"] + +[[test]] +name = "decimal" +required-features = ["rust_decimal", "bigdecimal03", "bigdecimal04"] + [package.metadata.docs.rs] all-features = true diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs index cd69b527..4ad2503f 100644 --- a/schemars/examples/schemars_attrs.rs +++ b/schemars/examples/schemars_attrs.rs @@ -10,6 +10,8 @@ pub struct MyStruct { pub my_bool: bool, #[schemars(default)] pub my_nullable_enum: Option, + #[schemars(inner(regex(pattern = "^x$")))] + pub my_vec_str: Vec, } #[derive(Deserialize, Serialize, JsonSchema)] diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json index 958cb6bb..9a6a22a6 100644 --- a/schemars/examples/schemars_attrs.schema.json +++ b/schemars/examples/schemars_attrs.schema.json @@ -4,7 +4,8 @@ "type": "object", "required": [ "myBool", - "myNumber" + "myNumber", + "myVecStr" ], "properties": { "myBool": { @@ -26,6 +27,13 @@ "format": "int32", "maximum": 10.0, "minimum": 1.0 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs index 35a734fa..e55cb7fc 100644 --- a/schemars/src/flatten.rs +++ b/schemars/src/flatten.rs @@ -171,8 +171,10 @@ fn is_null_type(schema: &Schema) -> bool { Schema::Object(s) => s, _ => return false, }; - match &s.instance_type { - Some(SingleOrVec::Single(t)) if **t == InstanceType::Null => true, - _ => false, - } + let instance_type = match &s.instance_type { + Some(SingleOrVec::Single(t)) => t, + _ => return false, + }; + + **instance_type == InstanceType::Null } diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs index 58eaa61e..a31839a0 100644 --- a/schemars/src/gen.rs +++ b/schemars/src/gen.rs @@ -11,6 +11,8 @@ use crate::schema::*; use crate::{visit::*, JsonSchema, Map}; use dyn_clone::DynClone; use serde::Serialize; +use std::borrow::Cow; +use std::collections::HashMap; use std::{any::Any, collections::HashSet, fmt::Debug}; /// Settings to customize how Schemas are generated. @@ -18,6 +20,7 @@ use std::{any::Any, collections::HashSet, fmt::Debug}; /// The default settings currently conform to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. /// If you require your generated schemas to conform to draft 7, consider using the [`draft07`](#method.draft07) method. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct SchemaSettings { /// If `true`, schemas for [`Option`](Option) will include a `nullable` property. /// @@ -45,7 +48,6 @@ pub struct SchemaSettings { /// /// Defaults to `false`. pub inline_subschemas: bool, - _hidden: (), } impl Default for SchemaSettings { @@ -64,7 +66,6 @@ impl SchemaSettings { meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()), visitors: vec![Box::new(RemoveRefSiblings)], inline_subschemas: false, - _hidden: (), } } @@ -77,7 +78,6 @@ impl SchemaSettings { meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()), visitors: Vec::default(), inline_subschemas: false, - _hidden: (), } } @@ -101,7 +101,6 @@ impl SchemaSettings { }), ], inline_subschemas: false, - _hidden: (), } } @@ -152,7 +151,9 @@ impl SchemaSettings { pub struct SchemaGenerator { settings: SchemaSettings, definitions: Map, - pending_schema_names: HashSet, + pending_schema_ids: HashSet>, + schema_id_to_name: HashMap, String>, + used_schema_names: HashSet, } impl Clone for SchemaGenerator { @@ -160,7 +161,9 @@ impl Clone for SchemaGenerator { Self { settings: self.settings.clone(), definitions: self.definitions.clone(), - pending_schema_names: HashSet::new(), + pending_schema_ids: HashSet::new(), + schema_id_to_name: HashMap::new(), + used_schema_names: HashSet::new(), } } } @@ -216,27 +219,54 @@ impl SchemaGenerator { /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// add them to the `SchemaGenerator`'s schema definitions. pub fn subschema_for(&mut self) -> Schema { - let name = T::schema_name(); + let id = T::schema_id(); let return_ref = T::is_referenceable() - && (!self.settings.inline_subschemas || self.pending_schema_names.contains(&name)); + && (!self.settings.inline_subschemas || self.pending_schema_ids.contains(&id)); if return_ref { - let reference = format!("{}{}", self.settings().definitions_path, name); + let name = match self.schema_id_to_name.get(&id).cloned() { + Some(n) => n, + None => { + let base_name = T::schema_name(); + let mut name = String::new(); + + if self.used_schema_names.contains(&base_name) { + for i in 2.. { + name = format!("{}{}", base_name, i); + if !self.used_schema_names.contains(&name) { + break; + } + } + } else { + name = base_name; + } + + self.used_schema_names.insert(name.clone()); + self.schema_id_to_name.insert(id.clone(), name.clone()); + name + } + }; + + let reference = format!("{}{}", self.settings.definitions_path, name); if !self.definitions.contains_key(&name) { - self.insert_new_subschema_for::(name); + self.insert_new_subschema_for::(name, id); } Schema::new_ref(reference) } else { - self.json_schema_internal::(&name) + self.json_schema_internal::(id) } } - fn insert_new_subschema_for(&mut self, name: String) { + fn insert_new_subschema_for( + &mut self, + name: String, + id: Cow<'static, str>, + ) { let dummy = Schema::Bool(false); // insert into definitions BEFORE calling json_schema to avoid infinite recursion self.definitions.insert(name.clone(), dummy); - let schema = self.json_schema_internal::(&name); + let schema = self.json_schema_internal::(id); self.definitions.insert(name, schema); } @@ -277,9 +307,8 @@ impl SchemaGenerator { /// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s /// [`definitions`](../schema/struct.Metadata.html#structfield.definitions) pub fn root_schema_for(&mut self) -> RootSchema { - let name = T::schema_name(); - let mut schema = self.json_schema_internal::(&name).into_object(); - schema.metadata().title.get_or_insert(name); + let mut schema = self.json_schema_internal::(T::schema_id()).into_object(); + schema.metadata().title.get_or_insert_with(T::schema_name); let mut root = RootSchema { meta_schema: self.settings.meta_schema.clone(), definitions: self.definitions.clone(), @@ -298,9 +327,8 @@ impl SchemaGenerator { /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions) pub fn into_root_schema_for(mut self) -> RootSchema { - let name = T::schema_name(); - let mut schema = self.json_schema_internal::(&name).into_object(); - schema.metadata().title.get_or_insert(name); + let mut schema = self.json_schema_internal::(T::schema_id()).into_object(); + schema.metadata().title.get_or_insert_with(T::schema_name); let mut root = RootSchema { meta_schema: self.settings.meta_schema, definitions: self.definitions, @@ -421,29 +449,29 @@ impl SchemaGenerator { } } - fn json_schema_internal(&mut self, name: &str) -> Schema { + fn json_schema_internal(&mut self, id: Cow<'static, str>) -> Schema { struct PendingSchemaState<'a> { gen: &'a mut SchemaGenerator, - name: &'a str, + id: Cow<'static, str>, did_add: bool, } impl<'a> PendingSchemaState<'a> { - fn new(gen: &'a mut SchemaGenerator, name: &'a str) -> Self { - let did_add = gen.pending_schema_names.insert(name.to_owned()); - Self { gen, name, did_add } + fn new(gen: &'a mut SchemaGenerator, id: Cow<'static, str>) -> Self { + let did_add = gen.pending_schema_ids.insert(id.clone()); + Self { gen, id, did_add } } } impl Drop for PendingSchemaState<'_> { fn drop(&mut self) { if self.did_add { - self.gen.pending_schema_names.remove(self.name); + self.gen.pending_schema_ids.remove(&self.id); } } } - let pss = PendingSchemaState::new(self, name); + let pss = PendingSchemaState::new(self, id); T::json_schema(pss.gen) } } diff --git a/schemars/src/json_schema_impls/array.rs b/schemars/src/json_schema_impls/array.rs index 558ddbd9..036d0423 100644 --- a/schemars/src/json_schema_impls/array.rs +++ b/schemars/src/json_schema_impls/array.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; // Does not require T: JsonSchema. impl JsonSchema for [T; 0] { @@ -10,6 +11,10 @@ impl JsonSchema for [T; 0] { "EmptyArray".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("[]") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::Array.into()), @@ -33,6 +38,11 @@ macro_rules! array_impls { format!("Array_size_{}_of_{}", $len, T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned( + format!("[{}; {}]", $len, T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::Array.into()), diff --git a/schemars/src/json_schema_impls/chrono.rs b/schemars/src/json_schema_impls/chrono.rs index 005c89b3..3a1731b5 100644 --- a/schemars/src/json_schema_impls/chrono.rs +++ b/schemars/src/json_schema_impls/chrono.rs @@ -3,6 +3,7 @@ use crate::schema::*; use crate::JsonSchema; use chrono::prelude::*; use serde_json::json; +use std::borrow::Cow; impl JsonSchema for Weekday { no_ref_schema!(); @@ -11,6 +12,10 @@ impl JsonSchema for Weekday { "Weekday".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("chrono::Weekday") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), @@ -41,6 +46,10 @@ macro_rules! formatted_string_impl { stringify!($ty).to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed(stringify!(chrono::$ty)) + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index 10f9f0f9..955ead67 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -2,6 +2,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; use serde_json::json; +use std::borrow::Cow; use std::ops::{Bound, Range, RangeInclusive}; impl JsonSchema for Option { @@ -11,6 +12,10 @@ impl JsonSchema for Option { format!("Nullable_{}", T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("Option<{}>", T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = gen.subschema_for::(); if gen.settings().option_add_null_type { @@ -69,6 +74,10 @@ impl JsonSchema for Result { format!("Result_of_{}_or_{}", T::schema_name(), E::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("Result<{}, {}>", T::schema_id(), E::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut ok_schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -99,6 +108,10 @@ impl JsonSchema for Bound { format!("Bound_of_{}", T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("Bound<{}>", T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut included_schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -139,6 +152,10 @@ impl JsonSchema for Range { format!("Range_of_{}", T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("Range<{}>", T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), diff --git a/schemars/src/json_schema_impls/decimal.rs b/schemars/src/json_schema_impls/decimal.rs index d7fc8a1f..643ca44d 100644 --- a/schemars/src/json_schema_impls/decimal.rs +++ b/schemars/src/json_schema_impls/decimal.rs @@ -1,22 +1,28 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; macro_rules! decimal_impl { ($type:ty) => { - decimal_impl!($type => Number, "Number"); - }; - ($type:ty => $instance_type:ident, $name:expr) => { impl JsonSchema for $type { no_ref_schema!(); fn schema_name() -> String { - $name.to_owned() + "Decimal".to_owned() + } + + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("Decimal") } fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), + instance_type: Some(InstanceType::String.into()), + string: Some(Box::new(StringValidation { + pattern: Some(r"^-?[0-9]+(\.[0-9]+)?$".to_owned()), + ..Default::default() + })), ..Default::default() } .into() @@ -27,5 +33,7 @@ macro_rules! decimal_impl { #[cfg(feature = "rust_decimal")] decimal_impl!(rust_decimal::Decimal); -#[cfg(feature = "bigdecimal")] -decimal_impl!(bigdecimal::BigDecimal); +#[cfg(feature = "bigdecimal03")] +decimal_impl!(bigdecimal03::BigDecimal); +#[cfg(feature = "bigdecimal04")] +decimal_impl!(bigdecimal04::BigDecimal); diff --git a/schemars/src/json_schema_impls/either.rs b/schemars/src/json_schema_impls/either.rs index 1010bc4e..957fdd16 100644 --- a/schemars/src/json_schema_impls/either.rs +++ b/schemars/src/json_schema_impls/either.rs @@ -2,6 +2,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; use either::Either; +use std::borrow::Cow; impl JsonSchema for Either { no_ref_schema!(); @@ -10,6 +11,14 @@ impl JsonSchema for Either { format!("Either_{}_or_{}", L::schema_name(), R::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!( + "either::Either<{}, {}>", + L::schema_id(), + R::schema_id() + )) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = SchemaObject::default(); schema.subschemas().any_of = Some(vec![gen.subschema_for::(), gen.subschema_for::()]); diff --git a/schemars/src/json_schema_impls/ffi.rs b/schemars/src/json_schema_impls/ffi.rs index 5ce10d75..55b50125 100644 --- a/schemars/src/json_schema_impls/ffi.rs +++ b/schemars/src/json_schema_impls/ffi.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; impl JsonSchema for OsString { @@ -8,6 +9,10 @@ impl JsonSchema for OsString { "OsString".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("std::ffi::OsString") + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut unix_schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), diff --git a/schemars/src/json_schema_impls/indexmap2.rs b/schemars/src/json_schema_impls/indexmap2.rs new file mode 100644 index 00000000..44eeedb3 --- /dev/null +++ b/schemars/src/json_schema_impls/indexmap2.rs @@ -0,0 +1,8 @@ +use crate::gen::SchemaGenerator; +use crate::schema::*; +use crate::JsonSchema; +use indexmap2::{IndexMap, IndexSet}; +use std::collections::{HashMap, HashSet}; + +forward_impl!(( JsonSchema for IndexMap) => HashMap); +forward_impl!(( JsonSchema for IndexSet) => HashSet); diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs index 356464fd..7c27808f 100644 --- a/schemars/src/json_schema_impls/maps.rs +++ b/schemars/src/json_schema_impls/maps.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; macro_rules! map_impl { ($($desc:tt)+) => { @@ -14,6 +15,10 @@ macro_rules! map_impl { format!("Map_of_{}", V::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned(format!("Map<{}>", V::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let subschema = gen.subschema_for::(); SchemaObject { diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs index 4952c242..0240b5db 100644 --- a/schemars/src/json_schema_impls/mod.rs +++ b/schemars/src/json_schema_impls/mod.rs @@ -17,6 +17,10 @@ macro_rules! forward_impl { <$target>::schema_name() } + fn schema_id() -> std::borrow::Cow<'static, str> { + <$target>::schema_id() + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { <$target>::json_schema(gen) } @@ -47,7 +51,11 @@ mod bytes; #[cfg(feature = "chrono")] mod chrono; mod core; -#[cfg(any(feature = "rust_decimal", feature = "bigdecimal"))] +#[cfg(any( + feature = "rust_decimal", + feature = "bigdecimal03", + feature = "bigdecimal04" +))] mod decimal; #[cfg(feature = "either")] mod either; @@ -56,10 +64,14 @@ mod enumset; mod ffi; #[cfg(feature = "indexmap")] mod indexmap; +#[cfg(feature = "indexmap2")] +mod indexmap2; mod maps; mod nonzero_signed; mod nonzero_unsigned; mod primitives; +#[cfg(feature = "semver")] +mod semver; mod sequences; mod serdejson; #[cfg(feature = "smallvec")] diff --git a/schemars/src/json_schema_impls/nonzero_signed.rs b/schemars/src/json_schema_impls/nonzero_signed.rs index 00bb63ff..c2fba2b2 100644 --- a/schemars/src/json_schema_impls/nonzero_signed.rs +++ b/schemars/src/json_schema_impls/nonzero_signed.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use std::num::*; macro_rules! nonzero_unsigned_impl { @@ -12,6 +13,10 @@ macro_rules! nonzero_unsigned_impl { stringify!($type).to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed(stringify!(std::num::$type)) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let zero_schema: Schema = SchemaObject { const_value: Some(0.into()), diff --git a/schemars/src/json_schema_impls/nonzero_unsigned.rs b/schemars/src/json_schema_impls/nonzero_unsigned.rs index 284ac8a6..1963d56e 100644 --- a/schemars/src/json_schema_impls/nonzero_unsigned.rs +++ b/schemars/src/json_schema_impls/nonzero_unsigned.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use std::num::*; macro_rules! nonzero_unsigned_impl { @@ -12,6 +13,10 @@ macro_rules! nonzero_unsigned_impl { stringify!($type).to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed(stringify!(std::num::$type)) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema: SchemaObject = <$primitive>::json_schema(gen).into(); schema.number().minimum = Some(1.0); diff --git a/schemars/src/json_schema_impls/primitives.rs b/schemars/src/json_schema_impls/primitives.rs index acdcd7e9..d1829848 100644 --- a/schemars/src/json_schema_impls/primitives.rs +++ b/schemars/src/json_schema_impls/primitives.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::path::{Path, PathBuf}; @@ -19,6 +20,10 @@ macro_rules! simple_impl { $name.to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed($name) + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::$instance_type.into()), @@ -64,6 +69,10 @@ macro_rules! unsigned_impl { $format.to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed($format) + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::$instance_type.into()), @@ -91,6 +100,10 @@ impl JsonSchema for char { "Character".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("char") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/schemars/src/json_schema_impls/semver.rs b/schemars/src/json_schema_impls/semver.rs new file mode 100644 index 00000000..dd8ed8ec --- /dev/null +++ b/schemars/src/json_schema_impls/semver.rs @@ -0,0 +1,30 @@ +use crate::gen::SchemaGenerator; +use crate::schema::*; +use crate::JsonSchema; +use semver::Version; +use std::borrow::Cow; + +impl JsonSchema for Version { + no_ref_schema!(); + + fn schema_name() -> String { + "Version".to_owned() + } + + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("semver::Version") + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + SchemaObject { + instance_type: Some(InstanceType::String.into()), + string: Some(Box::new(StringValidation { + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + pattern: Some(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$".to_owned()), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} diff --git a/schemars/src/json_schema_impls/sequences.rs b/schemars/src/json_schema_impls/sequences.rs index 58bb8e78..780307fa 100644 --- a/schemars/src/json_schema_impls/sequences.rs +++ b/schemars/src/json_schema_impls/sequences.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; macro_rules! seq_impl { ($($desc:tt)+) => { @@ -14,6 +15,11 @@ macro_rules! seq_impl { format!("Array_of_{}", T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned( + format!("[{}]", T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::Array.into()), @@ -41,6 +47,11 @@ macro_rules! set_impl { format!("Set_of_{}", T::schema_name()) } + fn schema_id() -> Cow<'static, str> { + Cow::Owned( + format!("Set<{}>", T::schema_id())) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::Array.into()), diff --git a/schemars/src/json_schema_impls/serdejson.rs b/schemars/src/json_schema_impls/serdejson.rs index d0cceff6..41eafd54 100644 --- a/schemars/src/json_schema_impls/serdejson.rs +++ b/schemars/src/json_schema_impls/serdejson.rs @@ -2,6 +2,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; use serde_json::{Map, Number, Value}; +use std::borrow::Cow; use std::collections::BTreeMap; impl JsonSchema for Value { @@ -11,6 +12,10 @@ impl JsonSchema for Value { "AnyValue".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("AnyValue") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { Schema::Bool(true) } @@ -25,6 +30,10 @@ impl JsonSchema for Number { "Number".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("Number") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::Number.into()), @@ -33,3 +42,6 @@ impl JsonSchema for Number { .into() } } + +#[cfg(feature = "raw_value")] +forward_impl!(serde_json::value::RawValue => Value); diff --git a/schemars/src/json_schema_impls/time.rs b/schemars/src/json_schema_impls/time.rs index f4362eef..767a1d2a 100644 --- a/schemars/src/json_schema_impls/time.rs +++ b/schemars/src/json_schema_impls/time.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use std::time::{Duration, SystemTime}; impl JsonSchema for Duration { @@ -8,6 +9,10 @@ impl JsonSchema for Duration { "Duration".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("std::time::Duration") + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), @@ -29,6 +34,10 @@ impl JsonSchema for SystemTime { "SystemTime".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("std::time::SystemTime") + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::Object.into()), diff --git a/schemars/src/json_schema_impls/tuple.rs b/schemars/src/json_schema_impls/tuple.rs index 383f839c..f67f06a6 100644 --- a/schemars/src/json_schema_impls/tuple.rs +++ b/schemars/src/json_schema_impls/tuple.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; macro_rules! tuple_impls { ($($len:expr => ($($name:ident)+))+) => { @@ -14,6 +15,14 @@ macro_rules! tuple_impls { name } + fn schema_id() -> Cow<'static, str> { + let mut id = "(".to_owned(); + id.push_str(&[$($name::schema_id()),+].join(",")); + id.push(')'); + + Cow::Owned(id) + } + fn json_schema(gen: &mut SchemaGenerator) -> Schema { let items = vec![ $(gen.subschema_for::<$name>()),+ diff --git a/schemars/src/json_schema_impls/url.rs b/schemars/src/json_schema_impls/url.rs index ffc5426f..be186121 100644 --- a/schemars/src/json_schema_impls/url.rs +++ b/schemars/src/json_schema_impls/url.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use url::Url; impl JsonSchema for Url { @@ -10,6 +11,10 @@ impl JsonSchema for Url { "Url".to_owned() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("url::Url") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/schemars/src/json_schema_impls/uuid08.rs b/schemars/src/json_schema_impls/uuid08.rs index 9e6dc587..b3b18f83 100644 --- a/schemars/src/json_schema_impls/uuid08.rs +++ b/schemars/src/json_schema_impls/uuid08.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use uuid08::Uuid; impl JsonSchema for Uuid { @@ -10,6 +11,10 @@ impl JsonSchema for Uuid { "Uuid".to_string() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("uuid::Uuid") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/schemars/src/json_schema_impls/uuid1.rs b/schemars/src/json_schema_impls/uuid1.rs index 85d67899..2e0c6e95 100644 --- a/schemars/src/json_schema_impls/uuid1.rs +++ b/schemars/src/json_schema_impls/uuid1.rs @@ -1,6 +1,7 @@ use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; +use std::borrow::Cow; use uuid1::Uuid; impl JsonSchema for Uuid { @@ -10,6 +11,10 @@ impl JsonSchema for Uuid { "Uuid".to_string() } + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("uuid::Uuid") + } + fn json_schema(_: &mut SchemaGenerator) -> Schema { SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index 31532979..f2332e58 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -1,288 +1,5 @@ #![forbid(unsafe_code)] -/*! -Generate JSON Schema documents from Rust code - -## Basic Usage - -If you don't really care about the specifics, the easiest way to generate a JSON schema for your types is to `#[derive(JsonSchema)]` and use the `schema_for!` macro. All fields of the type must also implement `JsonSchema` - Schemars implements this for many standard library types. - -```rust -use schemars::{schema_for, JsonSchema}; - -#[derive(JsonSchema)] -pub struct MyStruct { - pub my_int: i32, - pub my_bool: bool, - pub my_nullable_enum: Option, -} - -#[derive(JsonSchema)] -pub enum MyEnum { - StringNewType(String), - StructVariant { floats: Vec }, -} - -let schema = schema_for!(MyStruct); -println!("{}", serde_json::to_string_pretty(&schema).unwrap()); -``` - -
-Click to see the output JSON schema... - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MyStruct", - "type": "object", - "required": [ - "my_bool", - "my_int" - ], - "properties": { - "my_bool": { - "type": "boolean" - }, - "my_int": { - "type": "integer", - "format": "int32" - }, - "my_nullable_enum": { - "anyOf": [ - { - "$ref": "#/definitions/MyEnum" - }, - { - "type": "null" - } - ] - } - }, - "definitions": { - "MyEnum": { - "anyOf": [ - { - "type": "object", - "required": [ - "StringNewType" - ], - "properties": { - "StringNewType": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "StructVariant" - ], - "properties": { - "StructVariant": { - "type": "object", - "required": [ - "floats" - ], - "properties": { - "floats": { - "type": "array", - "items": { - "type": "number", - "format": "float" - } - } - } - } - }, - "additionalProperties": false - } - ] - } - } -} -``` -
- -### Serde Compatibility - -One of the main aims of this library is compatibility with [Serde](https://github.com/serde-rs/serde). Any generated schema *should* match how [serde_json](https://github.com/serde-rs/json) would serialize/deserialize to/from JSON. To support this, Schemars will check for any `#[serde(...)]` attributes on types that derive `JsonSchema`, and adjust the generated schema accordingly. - -```rust -use schemars::{schema_for, JsonSchema}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct MyStruct { - #[serde(rename = "myNumber")] - pub my_int: i32, - pub my_bool: bool, - #[serde(default)] - pub my_nullable_enum: Option, -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[serde(untagged)] -pub enum MyEnum { - StringNewType(String), - StructVariant { floats: Vec }, -} - -let schema = schema_for!(MyStruct); -println!("{}", serde_json::to_string_pretty(&schema).unwrap()); -``` - -
-Click to see the output JSON schema... - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MyStruct", - "type": "object", - "required": [ - "myBool", - "myNumber" - ], - "properties": { - "myBool": { - "type": "boolean" - }, - "myNullableEnum": { - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/MyEnum" - }, - { - "type": "null" - } - ] - }, - "myNumber": { - "type": "integer", - "format": "int32" - } - }, - "additionalProperties": false, - "definitions": { - "MyEnum": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object", - "required": [ - "floats" - ], - "properties": { - "floats": { - "type": "array", - "items": { - "type": "number", - "format": "float" - } - } - } - } - ] - } - } -} -``` -
- -`#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde. - -### Schema from Example Value - -If you want a schema for a type that can't/doesn't implement `JsonSchema`, but does implement `serde::Serialize`, then you can generate a JSON schema from a value of that type. However, this schema will generally be less precise than if the type implemented `JsonSchema` - particularly when it involves enums, since schemars will not make any assumptions about the structure of an enum based on a single variant. - -```rust -use schemars::schema_for_value; -use serde::Serialize; - -#[derive(Serialize)] -pub struct MyStruct { - pub my_int: i32, - pub my_bool: bool, - pub my_nullable_enum: Option, -} - -#[derive(Serialize)] -pub enum MyEnum { - StringNewType(String), - StructVariant { floats: Vec }, -} - -let schema = schema_for_value!(MyStruct { - my_int: 123, - my_bool: true, - my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string())) -}); -println!("{}", serde_json::to_string_pretty(&schema).unwrap()); -``` - -
-Click to see the output JSON schema... - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MyStruct", - "examples": [ - { - "my_bool": true, - "my_int": 123, - "my_nullable_enum": { - "StringNewType": "foo" - } - } - ], - "type": "object", - "properties": { - "my_bool": { - "type": "boolean" - }, - "my_int": { - "type": "integer" - }, - "my_nullable_enum": true - } -} -``` -
- -## Feature Flags -- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro -- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves -- `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject` - -Schemars can implement `JsonSchema` on types from several popular crates, enabled via feature flags (dependency versions are shown in brackets): -- `chrono` - [chrono](https://crates.io/crates/chrono) (^0.4) -- `indexmap1` - [indexmap](https://crates.io/crates/indexmap) (^1.2) -- `either` - [either](https://crates.io/crates/either) (^1.3) -- `uuid08` - [uuid](https://crates.io/crates/uuid) (^0.8) -- `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) -- `smallvec` - [smallvec](https://crates.io/crates/smallvec) (^1.0) -- `arrayvec05` - [arrayvec](https://crates.io/crates/arrayvec) (^0.5) -- `arrayvec07` - [arrayvec](https://crates.io/crates/arrayvec) (^0.7) -- `url` - [url](https://crates.io/crates/url) (^2.0) -- `bytes` - [bytes](https://crates.io/crates/bytes) (^1.0) -- `enumset` - [enumset](https://crates.io/crates/enumset) (^1.0) -- `rust_decimal` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0) -- `bigdecimal` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.3) -- `smol_str` - [smol_str](https://crates.io/crates/smol_str) (^0.1.17) -- `camino` - [camino](https://crates.io/crates/camino) (^1.1) - -For example, to implement `JsonSchema` on types from `chrono`, enable it as a feature in the `schemars` dependency in your `Cargo.toml` like so: - -```toml -[dependencies] -schemars = { version = "0.8", features = ["chrono"] } -``` -*/ +#![doc = include_str!("../README.md")] /// The map type used by schemars types. /// @@ -324,6 +41,8 @@ pub mod visit; #[cfg(feature = "schemars_derive")] extern crate schemars_derive; +use std::borrow::Cow; + #[cfg(feature = "schemars_derive")] pub use schemars_derive::*; @@ -339,7 +58,8 @@ use schema::Schema; /// /// This can also be automatically derived on most custom types with `#[derive(JsonSchema)]`. /// -/// # Example +/// # Examples +/// Deriving an implementation: /// ``` /// use schemars::{schema_for, JsonSchema}; /// @@ -350,6 +70,64 @@ use schema::Schema; /// /// let my_schema = schema_for!(MyStruct); /// ``` +/// +/// When manually implementing `JsonSchema`, as well as determining an appropriate schema, +/// you will need to determine an appropriate name and ID for the type. +/// For non-generic types, the type name/path are suitable for this: +/// ``` +/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +/// use std::borrow::Cow; +/// +/// struct NonGenericType; +/// +/// impl JsonSchema for NonGenericType { +/// fn schema_name() -> String { +/// // Exclude the module path to make the name in generated schemas clearer. +/// "NonGenericType".to_owned() +/// } +/// +/// fn schema_id() -> Cow<'static, str> { +/// // Include the module, in case a type with the same name is in another module/crate +/// Cow::Borrowed(concat!(module_path!(), "::NonGenericType")) +/// } +/// +/// fn json_schema(_gen: &mut SchemaGenerator) -> Schema { +/// todo!() +/// } +/// } +/// +/// assert_eq!(NonGenericType::schema_id(), <&mut NonGenericType>::schema_id()); +/// ``` +/// +/// But generic type parameters which may affect the generated schema should typically be included in the name/ID: +/// ``` +/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +/// use std::{borrow::Cow, marker::PhantomData}; +/// +/// struct GenericType(PhantomData); +/// +/// impl JsonSchema for GenericType { +/// fn schema_name() -> String { +/// format!("GenericType_{}", T::schema_name()) +/// } +/// +/// fn schema_id() -> Cow<'static, str> { +/// Cow::Owned(format!( +/// "{}::GenericType<{}>", +/// module_path!(), +/// T::schema_id() +/// )) +/// } +/// +/// fn json_schema(_gen: &mut SchemaGenerator) -> Schema { +/// todo!() +/// } +/// } +/// +/// assert_eq!(>::schema_id(), <&mut GenericType<&i32>>::schema_id()); +/// ``` +/// + pub trait JsonSchema { /// Whether JSON Schemas generated for this type should be re-used where possible using the `$ref` keyword. /// @@ -366,6 +144,17 @@ pub trait JsonSchema { /// This is used as the title for root schemas, and the key within the root's `definitions` property for subschemas. fn schema_name() -> String; + /// Returns a string that uniquely identifies the schema produced by this type. + /// + /// This does not have to be a human-readable string, and the value will not itself be included in generated schemas. + /// If two types produce different schemas, then they **must** have different `schema_id()`s, + /// but two types that produce identical schemas should *ideally* have the same `schema_id()`. + /// + /// The default implementation returns the same value as `schema_name()`. + fn schema_id() -> Cow<'static, str> { + Cow::Owned(Self::schema_name()) + } + /// Generates a JSON Schema for this type. /// /// If the returned schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 01fce865..4bef66e0 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -231,7 +231,7 @@ impl SchemaObject { self.reference.is_some() } - /// Returns `true` if `self` accepts values of the given type, according to the [`instance_type`] field. + /// Returns `true` if `self` accepts values of the given type, according to the [`instance_type`](struct.SchemaObject.html#structfield.instance_type) field. /// /// This is a basic check that always returns `true` if no `instance_type` is specified on the schema, /// and does not check any subschemas. Because of this, both `{}` and `{"not": {}}` accept any type according diff --git a/schemars/tests/decimal.rs b/schemars/tests/decimal.rs new file mode 100644 index 00000000..d245583b --- /dev/null +++ b/schemars/tests/decimal.rs @@ -0,0 +1,17 @@ +mod util; +use util::*; + +#[test] +fn rust_decimal() -> TestResult { + test_default_generated_schema::("rust_decimal") +} + +#[test] +fn bigdecimal03() -> TestResult { + test_default_generated_schema::("bigdecimal03") +} + +#[test] +fn bigdecimal04() -> TestResult { + test_default_generated_schema::("bigdecimal04") +} diff --git a/schemars/tests/expected/bigdecimal03.json b/schemars/tests/expected/bigdecimal03.json new file mode 100644 index 00000000..855db6f7 --- /dev/null +++ b/schemars/tests/expected/bigdecimal03.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "type": "string", + "pattern": "^-?[0-9]+(\\.[0-9]+)?$" +} \ No newline at end of file diff --git a/schemars/tests/expected/bigdecimal04.json b/schemars/tests/expected/bigdecimal04.json new file mode 100644 index 00000000..855db6f7 --- /dev/null +++ b/schemars/tests/expected/bigdecimal04.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "type": "string", + "pattern": "^-?[0-9]+(\\.[0-9]+)?$" +} \ No newline at end of file diff --git a/schemars/tests/expected/rust_decimal.json b/schemars/tests/expected/rust_decimal.json new file mode 100644 index 00000000..855db6f7 --- /dev/null +++ b/schemars/tests/expected/rust_decimal.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "type": "string", + "pattern": "^-?[0-9]+(\\.[0-9]+)?$" +} \ No newline at end of file diff --git a/schemars/tests/expected/same_name.json b/schemars/tests/expected/same_name.json new file mode 100644 index 00000000..9e4e6b34 --- /dev/null +++ b/schemars/tests/expected/same_name.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config2", + "type": "object", + "required": [ + "a_cfg", + "b_cfg" + ], + "properties": { + "a_cfg": { + "$ref": "#/definitions/Config" + }, + "b_cfg": { + "$ref": "#/definitions/Config2" + } + }, + "definitions": { + "Config": { + "type": "object", + "required": [ + "test" + ], + "properties": { + "test": { + "type": "string" + } + } + }, + "Config2": { + "type": "object", + "required": [ + "test2" + ], + "properties": { + "test2": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-name-const-generics.json b/schemars/tests/expected/schema-name-const-generics.json new file mode 100644 index 00000000..7505b285 --- /dev/null +++ b/schemars/tests/expected/schema-name-const-generics.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "const-generics-z-42", + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "integer", + "format": "int32" + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-name-mixed-generics.json b/schemars/tests/expected/schema-name-mixed-generics.json new file mode 100644 index 00000000..32ac797e --- /dev/null +++ b/schemars/tests/expected/schema-name-mixed-generics.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MixedGenericStruct_for_MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String_and_42_and_z", + "type": "object", + "required": [ + "foo", + "generic" + ], + "properties": { + "foo": { + "type": "integer", + "format": "int32" + }, + "generic": { + "$ref": "#/definitions/MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String" + } + }, + "definitions": { + "MySimpleStruct": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "integer", + "format": "int32" + } + } + }, + "MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String": { + "type": "object", + "required": [ + "inner", + "t", + "u", + "v", + "w" + ], + "properties": { + "inner": { + "$ref": "#/definitions/MySimpleStruct" + }, + "t": { + "type": "integer", + "format": "int32" + }, + "u": { + "type": "null" + }, + "v": { + "type": "boolean" + }, + "w": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/semver.json b/schemars/tests/expected/semver.json new file mode 100644 index 00000000..d87ad047 --- /dev/null +++ b/schemars/tests/expected/semver.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SemverTypes", + "type": "object", + "required": ["version"], + "properties": { + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } +} diff --git a/schemars/tests/expected/validate_inner.json b/schemars/tests/expected/validate_inner.json new file mode 100644 index 00000000..443a9fa6 --- /dev/null +++ b/schemars/tests/expected/validate_inner.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Struct", + "type": "object", + "required": [ + "array_str_length", + "slice_str_contains", + "vec_i32_range", + "vec_str_length", + "vec_str_length2", + "vec_str_regex", + "vec_str_url" + ], + "properties": { + "array_str_length": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 5 + }, + "maxItems": 2, + "minItems": 2 + }, + "slice_str_contains": { + "type": "array", + "items": { + "type": "string", + "pattern": "substring\\.\\.\\." + } + }, + "vec_i32_range": { + "type": "array", + "items": { + "type": "integer", + "format": "int32", + "maximum": 10.0, + "minimum": -10.0 + } + }, + "vec_str_length": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + }, + "vec_str_length2": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "maxItems": 3, + "minItems": 1 + }, + "vec_str_regex": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[Hh]ello\\b" + } + }, + "vec_str_url": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json index d4a14e3f..86658ac4 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Struct", + "title": "Struct2", "type": "object", "required": [ "contains_str1", diff --git a/schemars/tests/indexmap2.rs b/schemars/tests/indexmap2.rs new file mode 100644 index 00000000..efc77ddf --- /dev/null +++ b/schemars/tests/indexmap2.rs @@ -0,0 +1,16 @@ +mod util; +use indexmap2::{IndexMap, IndexSet}; +use schemars::JsonSchema; +use util::*; + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct IndexMapTypes { + map: IndexMap, + set: IndexSet, +} + +#[test] +fn indexmap_types() -> TestResult { + test_default_generated_schema::("indexmap") +} diff --git a/schemars/tests/same_name.rs b/schemars/tests/same_name.rs new file mode 100644 index 00000000..5e19611d --- /dev/null +++ b/schemars/tests/same_name.rs @@ -0,0 +1,35 @@ +mod util; +use schemars::JsonSchema; +use util::*; + +mod a { + use super::*; + + #[allow(dead_code)] + #[derive(JsonSchema)] + pub struct Config { + test: String, + } +} + +mod b { + use super::*; + + #[allow(dead_code)] + #[derive(JsonSchema)] + pub struct Config { + test2: String, + } +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Config2 { + a_cfg: a::Config, + b_cfg: b::Config, +} + +#[test] +fn same_name() -> TestResult { + test_default_generated_schema::("same_name") +} diff --git a/schemars/tests/schema_name.rs b/schemars/tests/schema_name.rs index 59fe0cff..ebd8a528 100644 --- a/schemars/tests/schema_name.rs +++ b/schemars/tests/schema_name.rs @@ -49,3 +49,29 @@ fn overriden_with_rename_multiple_type_params() -> TestResult { "schema-name-custom", ) } + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename = "const-generics-{BAR}-")] +struct ConstGenericStruct { + foo: i32, +} + +#[test] +fn overriden_with_rename_const_generics() -> TestResult { + test_default_generated_schema::>("schema-name-const-generics") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct MixedGenericStruct { + generic: T, + foo: i32, +} + +#[test] +fn default_name_mixed_generics() -> TestResult { + test_default_generated_schema::>, 42, 'z'>>( + "schema-name-mixed-generics", + ) +} diff --git a/schemars/tests/semver.rs b/schemars/tests/semver.rs new file mode 100644 index 00000000..617e5086 --- /dev/null +++ b/schemars/tests/semver.rs @@ -0,0 +1,15 @@ +mod util; +use schemars::JsonSchema; +use semver::Version; +use util::*; + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct SemverTypes { + version: Version, +} + +#[test] +fn semver_types() -> TestResult { + test_default_generated_schema::("semver") +} diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index e3817343..bf28d62c 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -101,7 +101,7 @@ pub struct Struct2 { #[test] fn validate_schemars_attrs() -> TestResult { - test_default_generated_schema::("validate_schemars_attrs") + test_default_generated_schema::("validate_schemars_attrs") } #[derive(JsonSchema)] diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs new file mode 100644 index 00000000..535410f1 --- /dev/null +++ b/schemars/tests/validate_inner.rs @@ -0,0 +1,31 @@ +mod util; + +use schemars::JsonSchema; +use util::*; + +// In real code, this would typically be a Regex, potentially created in a `lazy_static!`. +static STARTS_WITH_HELLO: &str = r"^[Hh]ello\b"; + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Struct<'a> { + #[schemars(inner(length(min = 5, max = 100)))] + array_str_length: [&'a str; 2], + #[schemars(inner(contains(pattern = "substring...")))] + slice_str_contains: &'a[&'a str], + #[schemars(inner(regex = "STARTS_WITH_HELLO"))] + vec_str_regex: Vec, + #[schemars(inner(length(min = 1, max = 100)))] + vec_str_length: Vec<&'a str>, + #[schemars(length(min = 1, max = 3), inner(length(min = 1, max = 100)))] + vec_str_length2: Vec, + #[schemars(inner(url))] + vec_str_url: Vec, + #[schemars(inner(range(min = -10, max = 10)))] + vec_i32_range: Vec, +} + +#[test] +fn validate_inner() -> TestResult { + test_default_generated_schema::("validate_inner") +} diff --git a/schemars_derive/Cargo.toml b/schemars_derive/Cargo.toml index 06caaa80..f09fc04d 100644 --- a/schemars_derive/Cargo.toml +++ b/schemars_derive/Cargo.toml @@ -3,12 +3,13 @@ name = "schemars_derive" description = "Macros for #[derive(JsonSchema)], for use with schemars" homepage = "https://graham.cool/schemars/" repository = "https://github.com/GREsau/schemars" -version = "0.8.12" +version = "0.8.15" authors = ["Graham Esau "] -edition = "2018" +edition = "2021" license = "MIT" readme = "README.md" keywords = ["rust", "json-schema", "serde"] +rust-version = "1.60" [lib] proc-macro = true diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 7b9052e4..250a3b2d 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -69,10 +69,7 @@ impl<'a> Variant<'a> { } pub fn is_unit(&self) -> bool { - match self.style { - serde_ast::Style::Unit => true, - _ => false, - } + matches!(self.style, serde_ast::Style::Unit) } } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index f790694a..f95bc485 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -103,11 +103,7 @@ impl Attrs { } }; - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { + for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) { match &meta_item { Meta(NameValue(m)) if m.path.is_ident("with") => { if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) { @@ -167,6 +163,12 @@ impl Attrs { _ if ignore_errors => {} + Meta(List(m)) if m.path.is_ident("inner") && attr_type == "schemars" => { + // This will be processed with the validation attributes. + // It's allowed only for the schemars attribute because the + // validator crate doesn't support it yet. + } + Meta(meta_item) => { if !is_known_serde_or_validation_keyword(meta_item) { let path = meta_item @@ -190,8 +192,7 @@ impl Attrs { } pub fn is_default(&self) -> bool { - match self { - Self { + matches!(self, Self { with: None, title: None, description: None, @@ -200,9 +201,7 @@ impl Attrs { repr: None, crate_name: None, is_renamed: _, - } if examples.is_empty() => true, - _ => false, - } + } if examples.is_empty()) } } @@ -217,30 +216,25 @@ fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool { } fn get_meta_items( - attr: &syn::Attribute, + attrs: &[syn::Attribute], attr_type: &'static str, errors: &Ctxt, ignore_errors: bool, -) -> Result, ()> { - if !attr.path.is_ident(attr_type) { - return Ok(Vec::new()); - } - - match attr.parse_meta() { - Ok(List(meta)) => Ok(meta.nested.into_iter().collect()), - Ok(other) => { - if !ignore_errors { - errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)) - } - Err(()) +) -> Vec { + attrs.iter().fold(vec![], |mut acc, attr| { + if !attr.path.is_ident(attr_type) { + return acc; } - Err(err) => { - if !ignore_errors { - errors.error_spanned_by(attr, err) + match attr.parse_meta() { + Ok(List(meta)) => acc.extend(meta.nested), + Ok(other) if !ignore_errors => { + errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)) } - Err(()) + Err(err) if !ignore_errors => errors.error_spanned_by(attr, err), + _ => (), } - } + acc + }) } fn get_lit_str<'a>( diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index a089a1de..ec7cc145 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -43,13 +43,17 @@ pub struct ValidationAttrs { contains: Option, required: bool, format: Option, + inner: Option>, } impl ValidationAttrs { pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { + let schemars_items = get_meta_items(attrs, "schemars", errors, false); + let validate_items = get_meta_items(attrs, "validate", errors, true); + ValidationAttrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "validate", true, errors) + .populate(schemars_items, "schemars", false, errors) + .populate(validate_items, "validate", true, errors) } pub fn required(&self) -> bool { @@ -58,7 +62,7 @@ impl ValidationAttrs { fn populate( mut self, - attrs: &[syn::Attribute], + meta_items: Vec, attr_type: &'static str, ignore_errors: bool, errors: &Ctxt, @@ -97,11 +101,7 @@ impl ValidationAttrs { } }; - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { + for meta_item in meta_items { match &meta_item { NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => { for nested in meta_list.nested.iter() { @@ -247,7 +247,8 @@ impl ValidationAttrs { if !ignore_errors { errors.error_spanned_by( meta, - "unknown item in schemars regex attribute".to_string(), + "unknown item in schemars regex attribute" + .to_string(), ); } } @@ -292,7 +293,8 @@ impl ValidationAttrs { if !ignore_errors { errors.error_spanned_by( meta, - "unknown item in schemars contains attribute".to_string(), + "unknown item in schemars contains attribute" + .to_string(), ); } } @@ -302,6 +304,21 @@ impl ValidationAttrs { } } + NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("inner") => { + match self.inner { + Some(_) => duplicate_error(&meta_list.path), + None => { + let inner_attrs = ValidationAttrs::default().populate( + meta_list.nested.clone().into_iter().collect(), + attr_type, + ignore_errors, + errors, + ); + self.inner = Some(Box::new(inner_attrs)); + } + } + } + _ => {} } } @@ -309,16 +326,24 @@ impl ValidationAttrs { } pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { + if let Some(apply_expr) = self.apply_to_schema_expr() { + *schema_expr = quote! { + { + let mut schema = #schema_expr; + #apply_expr + schema + } + } + } + } + + fn apply_to_schema_expr(&self) -> Option { let mut array_validation = Vec::new(); let mut number_validation = Vec::new(); let mut object_validation = Vec::new(); let mut string_validation = Vec::new(); - if let Some(length_min) = self - .length_min - .as_ref() - .or(self.length_equal.as_ref()) - { + if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) { string_validation.push(quote! { validation.min_length = Some(#length_min as u32); }); @@ -327,11 +352,7 @@ impl ValidationAttrs { }); } - if let Some(length_max) = self - .length_max - .as_ref() - .or(self.length_equal.as_ref()) - { + if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) { string_validation.push(quote! { validation.max_length = Some(#length_max as u32); }); @@ -378,6 +399,21 @@ impl ValidationAttrs { } }); + let inner_validation = self + .inner + .as_deref() + .and_then(|inner| inner.apply_to_schema_expr()) + .map(|apply_expr| { + quote! { + if schema_object.has_type(schemars::schema::InstanceType::Array) { + if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items { + let mut schema = &mut **inner_schema; + #apply_expr + } + } + } + }); + let array_validation = wrap_array_validation(array_validation); let number_validation = wrap_number_validation(number_validation); let object_validation = wrap_object_validation(object_validation); @@ -388,21 +424,20 @@ impl ValidationAttrs { || object_validation.is_some() || string_validation.is_some() || format.is_some() + || inner_validation.is_some() { - *schema_expr = quote! { - { - let mut schema = #schema_expr; - if let schemars::schema::Schema::Object(schema_object) = &mut schema - { - #array_validation - #number_validation - #object_validation - #string_validation - #format - } - schema + Some(quote! { + if let schemars::schema::Schema::Object(schema_object) = &mut schema { + #array_validation + #number_validation + #object_validation + #string_validation + #format + #inner_validation } - } + }) + } else { + None } } } diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index f8173f42..3ad10319 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -67,6 +67,10 @@ fn derive_json_schema( <#ty as schemars::JsonSchema>::schema_name() } + fn schema_id() -> std::borrow::Cow<'static, str> { + <#ty as schemars::JsonSchema>::schema_id() + } + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { <#ty as schemars::JsonSchema>::json_schema(gen) } @@ -95,27 +99,70 @@ fn derive_json_schema( // FIXME improve handling of generic type params which may not implement JsonSchema let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect(); - let schema_name = - if type_params.is_empty() || (cont.attrs.is_renamed && !schema_base_name.contains('{')) { + let const_params: Vec<_> = cont.generics.const_params().map(|c| &c.ident).collect(); + let params: Vec<_> = type_params.iter().chain(const_params.iter()).collect(); + + let (schema_name, schema_id) = if params.is_empty() + || (cont.attrs.is_renamed && !schema_base_name.contains('{')) + { + ( quote! { #schema_base_name.to_owned() - } - } else if cont.attrs.is_renamed { - let mut schema_name_fmt = schema_base_name; - for tp in &type_params { - schema_name_fmt.push_str(&format!("{{{}:.0}}", tp)); - } + }, quote! { - format!(#schema_name_fmt #(,#type_params=#type_params::schema_name())*) - } - } else { - let mut schema_name_fmt = schema_base_name; - schema_name_fmt.push_str("_for_{}"); - schema_name_fmt.push_str(&"_and_{}".repeat(type_params.len() - 1)); + std::borrow::Cow::Borrowed(std::concat!( + std::module_path!(), + "::", + #schema_base_name + )) + }, + ) + } else if cont.attrs.is_renamed { + let mut schema_name_fmt = schema_base_name; + for tp in ¶ms { + schema_name_fmt.push_str(&format!("{{{}:.0}}", tp)); + } + ( quote! { - format!(#schema_name_fmt #(,#type_params::schema_name())*) - } - }; + format!(#schema_name_fmt #(,#type_params=#type_params::schema_name())* #(,#const_params=#const_params)*) + }, + quote! { + std::borrow::Cow::Owned( + format!( + std::concat!( + std::module_path!(), + "::", + #schema_name_fmt + ) + #(,#type_params=#type_params::schema_id())* + #(,#const_params=#const_params)* + ) + ) + }, + ) + } else { + let mut schema_name_fmt = schema_base_name; + schema_name_fmt.push_str("_for_{}"); + schema_name_fmt.push_str(&"_and_{}".repeat(params.len() - 1)); + ( + quote! { + format!(#schema_name_fmt #(,#type_params::schema_name())* #(,#const_params)*) + }, + quote! { + std::borrow::Cow::Owned( + format!( + std::concat!( + std::module_path!(), + "::", + #schema_name_fmt + ) + #(,#type_params::schema_id())* + #(,#const_params)* + ) + ) + }, + ) + }; let schema_expr = if repr { schema_exprs::expr_for_repr(&cont).map_err(|e| vec![e])? @@ -134,6 +181,10 @@ fn derive_json_schema( #schema_name } + fn schema_id() -> std::borrow::Cow<'static, str> { + #schema_id + } + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { #schema_expr } diff --git a/schemars_derive/src/regex_syntax.rs b/schemars_derive/src/regex_syntax.rs index 750d1e2b..11cd638d 100644 --- a/schemars_derive/src/regex_syntax.rs +++ b/schemars_derive/src/regex_syntax.rs @@ -1,5 +1,6 @@ +#![allow(clippy::all)] // Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function -// https://github.com/rust-lang/regex/blob/ff283badce21dcebd581909d38b81f2c8c9bfb54/regex-syntax/src/lib.rs +// https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs // // Copyright (c) 2014 The Rust Project Developers // diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 1f55084b..9bd81325 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -109,6 +109,15 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option) { #fn_name.to_string() } + fn schema_id() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(std::concat!( + "_SchemarsSchemaWithFunction/", + std::module_path!(), + "/", + #fn_name + )) + } + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { #fun(gen) }