diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ed3bff4..e46e943a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,28 +5,21 @@ on: [push, pull_request, workflow_dispatch] jobs: ci: 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' }} strategy: matrix: rust: - - 1.45.0 + - 1.65.0 - stable - beta - nightly include: - - rust: 1.45.0 - test_features: "--features impl_json_schema" + - rust: 1.65.0 allow_failure: false - rust: stable - test_features: "--all-features" allow_failure: false - rust: beta - test_features: "--all-features" allow_failure: false - rust: nightly - test_features: "--all-features" allow_failure: true fail-fast: false steps: @@ -36,15 +29,12 @@ 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 }} working-directory: ./schemars - name: Run tests - run: cargo test --verbose ${{ matrix.test_features }} --no-fail-fast + run: cargo test --verbose --all-features --no-fail-fast continue-on-error: ${{ matrix.allow_failure }} working-directory: ./schemars - name: Run derive tests diff --git a/.gitignore b/.gitignore index 088ba6ba..f2e972dd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,5 @@ # will have compiled files and executables /target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d1f8d25e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.check.command": "clippy", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.testExplorer": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f59e662..98754dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,90 +1,354 @@ # Changelog +## [1.0.0-alpha.14] - 2024-08-29 + +### Added + +- Read `#[garde(...)]` attributes as an alternative to `#[validate(...)]` (https://github.com/GREsau/schemars/issues/233 / https://github.com/GREsau/schemars/pull/331). See [the documentation](https://graham.cool/schemars/deriving/attributes/#supported-validatorgarde-attributes) for a full list of supported attributes. + +## [1.0.0-alpha.13] - 2024-08-27 + +### Fixed + +- Fix compile errors when using `#[validate(regex(path = *expr))]` attribute + +## [1.0.0-alpha.12] - 2024-08-27 + +### Fixed + +- Allow `regex(path = ...)` value to be a non-string expression (https://github.com/GREsau/schemars/issues/302 / https://github.com/GREsau/schemars/pull/328) +- Respect `#[serde(rename_all_fields = ...)]` attribute (https://github.com/GREsau/schemars/issues/273 / https://github.com/GREsau/schemars/pull/304) + +### Changed (_⚠️ possibly-breaking changes ⚠️_) + +- Invalid attributes that were previously silently ignored (e.g. setting `schema_with` on structs) will now cause compile errors +- Validation attribute parsing has been altered to match the latest version of the validator crate: + - Remove the `phone` attribute + - Remove the `required_nested` attribute + - `regex` and `contains` attributes must now be specified in list form `#[validate(regex(path = ...))]` rather than name/value form `#[validate(regex = ...)]` + +## [1.0.0-alpha.11] - 2024-08-24 + +### Changed + +- Values in `#[doc = ...]` and `#[schemars(description = ..., title = ...)]` attributes may now be any arbitrary expression rather than just string literals. (https://github.com/GREsau/schemars/issues/204 / https://github.com/GREsau/schemars/pull/327) +- ⚠️ MSRV is now 1.65 ⚠️ + +## [1.0.0-alpha.10] - 2024-08-22 + +### Fixed + +- Fix some cases of unsatisfiable schemas generated when flattening enums (https://github.com/GREsau/schemars/pull/325 / https://github.com/GREsau/schemars/issues/164 / https://github.com/GREsau/schemars/issues/165) + +## [1.0.0-alpha.9] - 2024-08-21 + +### Added + +- Add rustdoc for `derive(JsonSchema)` macro (https://github.com/GREsau/schemars/issues/322 / https://github.com/GREsau/schemars/issues/322) + +## [1.0.0-alpha.8] - 2024-08-21 + +### Changed + +- Replace `schemars::gen` module with `schemars::generate`. This is because `gen` is a reserved keyword in rust 2024, so can only be used as `r#gen`. The `schemars::gen` module is still available for ease of upgrading, but is marked as deprecated and _may_ be removed in the future 1.0.0 release. (https://github.com/GREsau/schemars/issues/306 / https://github.com/GREsau/schemars/pull/323) + +## [1.0.0-alpha.7] - 2024-08-19 + +### Fixed + +- Fix behaviour of `flatten` for schemas with `additionalProperties` +- Fix behaviour of `flatten` of multiple enums (https://github.com/GREsau/schemars/issues/165 / https://github.com/GREsau/schemars/pull/320) + +## [1.0.0-alpha.6] - 2024-08-17 + +### Fixed + +- Fixed a configuration error that caused rustdoc generation to fail on docs.rs + +## [1.0.0-alpha.5] - 2024-08-17 + +### Added + +- Schemars can now be used in `no_std` environments by disabling the new `std` feature flag (which is enabled by default). Schemars still requires an allocator to be available. + +## [1.0.0-alpha.4] - 2024-08-17 + +### Fixed + +- Reduce size of MIR output (and improve release-mode compile time) when deriving `JsonSchema` involving applying schema metadata +- Fix `flatten`ing of `serde_json::Value` +- Use absolute import for `Result` in derive output, ignoring any locally imported types called `Result` (https://github.com/GREsau/schemars/pull/307) + +## [1.0.0-alpha.3] - 2024-08-10 + +### Added + +- `#[schemars(transform = some::transform)]` for applying arbitrary modifications to generated schemas. `some::transform` must be an expression of type `schemars::transform::Transform` - note that this can be a function with the signature `fn(&mut Schema) -> ()`. +- `SchemaSettings` and `SchemaGenerator` are both now `Send` + +### Changed (_⚠️ breaking changes ⚠️_) + +- `visit` module and `Visitor` trait have been replace with `transform` and `Transform` respectively. Accordingly, these items have been renamed: + - `SchemaSettings::visitors` -> `SchemaSettings::transforms` + - `SchemaSettings::with_visitor` -> `SchemaSettings::with_transform` + - `SchemaGenerator::visitors_mut` -> `SchemaGenerator::transforms_mut` + - `GenVisitor` -> `GenTransform` + - `Visitor::visit_schema` -> `Transform::transform` + - `visit::visit_schema` -> `transform::transform_subschemas` +- `GenTransform` must also impl `Send`, but no longer needs to impl `Debug` +- Doc comments no longer have newlines collapsed when generating the `description` property (https://github.com/GREsau/schemars/pull/310) + +## [1.0.0-alpha.2] - 2024-06-05 + +### Added + +- `#[schemars(extend("key" = value))]` attribute which can be used to add properties (or replace existing properties) in a generated schema (https://github.com/GREsau/schemars/issues/50 / https://github.com/GREsau/schemars/pull/297) + - Can be set on a struct, enum, or enum variant + - Value can be any expression that results in a value implementing `Serialize` + - Value can also be a JSON literal following the rules of `serde_json::json!(value)` macro, i.e. it can interpolate other values that implement `Serialize` + +## [1.0.0-alpha.1] - 2024-05-27 + +### Added + +- `json_schema!` macro for creating a custom `Schema` +- Implement `JsonSchema` for [uuid](https://crates.io/crates/uuid) 1.x types, under the optional `uuid1` feature flag +- `SchemaSettings::draft2020_12()` to construct settings conforming to [JSON Schema draft 2020-12](https://json-schema.org/draft/2020-12/release-notes) + +### Changed (_⚠️ breaking changes ⚠️_) + +- The `Schema` type is now defined as a thin wrapper around a `serde_json::Value` +- The default `SchemaSettings` (used by the `schema_for!()`/`schema_for_value!()` macros and `SchemaGenerator::default()`) now conform to JSON Schema draft 2020-12 instead of draft 7. +- Schemas generated using `SchemaSettings::draft2019_09()` (and `draft2020_12()` and `default()`) now use `$defs` instead of `definitions`. While using `definitions` is allowed by the spec, `$defs` is the preferred property for storing reusable schemas. +- `JsonSchema::schema_name()` now returns `Cow<'static, str>` instead of `String` +- `JsonSchema::is_referenceable()` has been removed, and replaced with the more clearly-named `JsonSchema::always_inline()` (which should returns the **opposite** value to what `is_referenceable` returned!) +- The `SchemaGenerator.definitions` field is now a `serde_json::Map` +- Macros/functions that previously returned a `RootSchema` now return a `Schema` instead +- All optional dependencies are now suffixed by their version: + - `chrono` is now `chrono04` + - `either` is now `either1` + - `smallvec` is now `smallvec1` + - `url` is now `url2` + - `bytes` is now `bytes1` + - `rust_decimal` is now `rust_decimal1` + - `enumset` is now `enumset1` + - `smol_str` is now `smol_str02` + - `semver` is now `semver1` + - `indexmap2`, `arrayvec07` and `bigdecimal04` are unchanged + +### Removed (_⚠️ breaking changes ⚠️_) + +- Removed deprecated `SchemaGenerator` methods `make_extensible`, `schema_for_any` and `schema_for_none` +- Removed the `schema` module + - The `Schema` type is now accessible from the crate root (i.e. `schemars::Schema` instead of `schemars::schema::Schema`) + - All other types that were in the module have been removed: + - `RootSchema` + - `SchemaObject` + - `Metadata` + - `SubschemaValidation` + - `NumberValidation` + - `StringValidation` + - `ArrayValidation` + - `ObjectValidation` + - `InstanceType` + - `SingleOrVec` +- Removed `schemars::Set` and `schemars::Map` type aliases +- Removed the `impl_json_schema` feature flag - `JsonSchema` is now always implemented on `Schema` +- Remove methods `visit_schema_object` and `visit_root_schema` from the `Visitor` trait (`visit_schema` is unchanged) + - Visitors that previously defined `visit_schema_object` should instead define `visit_schema` and use an `if let Some(obj) = schema.as_object_mut()` or similar construct +- Old versions of optional dependencies have been removed - all of these have newer versions (shown in brackets) which are supported by schemars + - `indexmap` (consider using `indexmap2`) + - `uuid08` (consider using `uuid1`) + - `arrayvec05` (consider using `arrayvec07`) + - `bigdecimal03` (consider using `bigdecimal04`) +- Remove the retain_examples field from SetSingleExample, which is now a unit struct + +## [0.8.21] - 2024-05-23 + +### Fixed: + +- Fix `null` default not being set on generated schemas (https://github.com/GREsau/schemars/issues/295 / https://github.com/GREsau/schemars/pull/296) + +## [0.8.20] - 2024-05-18 + +### Fixed: + +- Revert unintentional change in behaviour when combining `default` and `required` attributes (https://github.com/GREsau/schemars/issues/292) + +## [0.8.19] - 2024-05-06 + +### Fixed: + +- Regression that caused a compile error when deriving `JsonSchema` on an enum with no variants (https://github.com/GREsau/schemars/issues/287) + +## [0.8.18] - 2024-05-06 + +### Fixed: + +- Reduce size of MIR output (and improve release-mode compile time) when deriving `JsonSchema` on enums (https://github.com/GREsau/schemars/pull/266 / https://github.com/GREsau/schemars/pull/286) + +## [0.8.17] - 2024-04-28 + +### Changed: + +- Update to syn 2.0, which should improve compile times in many cases (https://github.com/GREsau/schemars/pull/281) + +## [0.8.16] - 2023-11-11 + +### Fixed: + +- Reduce size of MIR output (and improve release-mode compile time) when deriving `JsonSchema` + +## [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 +357,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.lock b/Cargo.lock new file mode 100644 index 00000000..f39bca27 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,532 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "basic-toml" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6" +dependencies = [ + "serde", +] + +[[package]] +name = "bigdecimal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06619be423ea5bb86c95f087d5707942791a08a85530df0db2209a3ecfb8bc9" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "num-traits", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "dyn-clone" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "enumset" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ref-cast" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rust_decimal" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schemars" +version = "1.0.0-alpha.14" +dependencies = [ + "arrayvec", + "bigdecimal", + "bytes", + "chrono", + "dyn-clone", + "either", + "enumset", + "indexmap", + "pretty_assertions", + "ref-cast", + "rust_decimal", + "schemars_derive", + "semver", + "serde", + "serde_json", + "smallvec", + "smol_str", + "trybuild", + "url", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "1.0.0-alpha.14" +dependencies = [ + "pretty_assertions", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "smol_str" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "trybuild" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196a58260a906cedb9bf6d8034b6379d0c11f552416960452f267402ceeddff1" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 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 7ff05e0f..4e4b1643 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Schemars +> [!NOTE] +> This branch is for the current v1 alpha version of Schemars which is still under development. +> For the current stable release of Schemars (v0.8.x), see the [v0 branch](https://github.com/GREsau/schemars/tree/v0). +> +> For information on migrating from 0.8 to 1.0, see [the migration guide](https://graham.cool/schemars/migrating/). + [![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) +[![Docs](https://img.shields.io/docsrs/schemars/1.0.0--latest?label=docs)](https://docs.rs/schemars/1.0.0--latest) +[![MSRV 1.65+](https://img.shields.io/badge/msrv-1.65-blue)](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html) Generate JSON Schema documents from Rust code @@ -36,13 +42,9 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); ```json { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "type": "boolean" @@ -54,7 +56,7 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "my_nullable_enum": { "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" @@ -62,32 +64,25 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); ] } }, - "definitions": { + "required": ["my_int", "my_bool"], + "$defs": { "MyEnum": { - "anyOf": [ + "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": ["StringNewType"] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -96,21 +91,24 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "format": "float" } } - } + }, + "required": ["floats"] } }, - "additionalProperties": false + "additionalProperties": false, + "required": ["StructVariant"] } ] } } } ``` + ### 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}; @@ -142,27 +140,23 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); ```json { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { - "default": null, "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" } - ] + ], + "default": null }, "myNumber": { "type": "integer", @@ -170,7 +164,8 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } }, "additionalProperties": false, - "definitions": { + "required": ["myNumber", "myBool"], + "$defs": { "MyEnum": { "anyOf": [ { @@ -178,9 +173,6 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); }, { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -189,13 +181,15 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); "format": "float" } } - } + }, + "required": ["floats"] } ] } } } ``` + `#[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,32 +251,35 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } } ``` + ## Feature Flags + +- `std` (enabled by default) - implements `JsonSchema` for types in the rust standard library (`JsonSchema` is still implemented on types in `core` and `alloc`, even when this feature is disabled). Disable this feature to use schemars in `no_std` environments. - `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` +- `preserve_order` - keep the order of struct fields in `Schema` properties +- `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) -- `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) +- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4) +- `bytes1` - [bytes](https://crates.io/crates/bytes) (^1.0) +- `chrono04` - [chrono](https://crates.io/crates/chrono) (^0.4) +- `either1` - [either](https://crates.io/crates/either) (^1.3) +- `enumset1` - [enumset](https://crates.io/crates/enumset) (^1.0) +- `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0) +- `rust_decimal1` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0) +- `semver1` - [semver](https://crates.io/crates/semver) (^1.0.9) +- `smallvec1` - [smallvec](https://crates.io/crates/smallvec) (^1.0) +- `smol_str02` - [smol_str](https://crates.io/crates/smol_str) (^0.2.1) +- `url2` - [url](https://crates.io/crates/url) (^2.0) +- `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) 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"] } +schemars = { version = "1.0.0-alpha.14", features = ["chrono04"] } ``` diff --git a/clippy.toml b/clippy.toml index 90bfd5f6..2753d24e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.45.0" +doc-valid-idents = ["OpenAPI", "serde_json", ".."] diff --git a/docs-rs-custom.css b/docs-rs-custom.css new file mode 100644 index 00000000..a7807d8c --- /dev/null +++ b/docs-rs-custom.css @@ -0,0 +1,4 @@ +/* Hide the `[!NOTE] This branch...` alert at the top of the README, as it's only applicable to users on GitHub */ +h2#schemars + blockquote { + display: none; +} diff --git a/docs/0-migrating.md b/docs/0-migrating.md new file mode 100644 index 00000000..80fccb38 --- /dev/null +++ b/docs/0-migrating.md @@ -0,0 +1,189 @@ +--- +title: Migrating from 0.8 +nav_order: 2 +has_children: true +has_toc: false +permalink: /migrating/ +layout: default +--- + +# Migrating from 0.8 to 1.0 + +
+

Schemars 1.0 is still under development, and further changes may be introduced. +

+ +## Optional dependencies + +All optional dependencies are now suffixed by their version: + +- `chrono` is now `chrono04` +- `either` is now `either1` +- `smallvec` is now `smallvec1` +- `url` is now `url2` +- `bytes` is now `bytes1` +- `rust_decimal` is now `rust_decimal1` +- `enumset` is now `enumset1` +- `smol_str` is now `smol_str02` +- `semver` is now `semver1` +- `indexmap`, `uuid08`, `arrayvec05` and `bigdecimal03` have been removed +- `indexmap2`, `arrayvec07` and `bigdecimal04` are unchanged + +## `Schema` is now a wrapper around `serde_json::Value` + +`Schema` is now defined as a wrapper around a `serde_json::Value` (which must be a `Value::Bool` or `Value::Object`), rather than a struct with a field for each JSON schema keyword (with some intermediary types). `Schema` is now available as `schemars::Schema` instead of `schemars::schema::Schema`, and all other types that were in the `schemars::schema` module have now been removed. Functions that previously returned a `RootSchema` now just return a `Schema`. + +A new macro `json_schema!(...)` is available to easily create new instances of `Schema`, which functions similarly to the [`serde_json::json!(...)` macro](https://docs.rs/serde_json/latest/serde_json/macro.json.html). + +Here's how you might create and modify a `Schema` in schemars v0.8: + +```rust +use schemars::schema::{InstanceType, ObjectValidation, Schema, SchemaObject}; +use schemars::Map; + +// Create a Schema for an object with property `foo` +let schema_object = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: Map::from_iter([("foo".to_owned(), true.into())]), + ..Default::default() + })), + ..Default::default() +}; +let schema: Schema = schema_object.into(); + +// Make the `foo` property required +let mut schema_object = schema.into_object(); +let obj = schema_object.object(); +obj.required.insert("foo".to_owned()); +``` + +And the same thing in v1.0: + +```rust +use schemars::{json_schema, Schema}; + +// Create a Schema for an object with property `foo` +let mut schema: Schema = json_schema!({ + "type": "object", + "properties": { + "foo": true + } +}); + +// Make the `foo` property required +schema + .ensure_object() + .entry("required") + .or_insert(serde_json::Value::Array(Vec::new())) + .as_array_mut() + .expect("`required` should be an array") + .push("foo".into()); +``` + +## `visit::Visitor` replaced with `transform::Transform` + +The `visit` module and `Visitor` trait have been replace with `transform` and `Transform` respectively. Accordingly, these items have been renamed: + +- `SchemaSettings::visitors` -> `SchemaSettings::transforms` +- `SchemaSettings::with_visitor` -> `SchemaSettings::with_transform` +- `SchemaGenerator::visitors_mut` -> `SchemaGenerator::transforms_mut` +- `GenVisitor` -> `GenTransform` +- `Visitor::visit_schema` -> `Transform::transform` + - `visit_schema_object` and `visit_root_schema` methods have been removed +- `visit::visit_schema` -> `transform::transform_subschemas` + - `visit_schema_object` and `visit_root_schema` functions have been removed + +So if you had defined this `Visitor` in schemars 0.8: + +```rust +use schemars::schema::SchemaObject; +use schemars::visit::{visit_schema_object, Visitor}; + +pub struct MyVisitor; + +impl Visitor for MyVisitor { + fn visit_schema_object(&mut self, schema: &mut SchemaObject) { + // First, make our change to this schema + schema + .extensions + .insert("my_property".to_string(), serde_json::json!("hello world")); + + // Then delegate to default implementation to visit any subschemas + visit_schema_object(self, schema); + } +} + +let mut schema = schemars::schema_for!(str); +MyVisitor.visit_root_schema(&mut schema); +``` + +Then the equivalent `Transform` in schemars 1.0 would be: + +```rust +use schemars::transform::{transform_subschemas, Transform}; +use schemars::Schema; + +pub struct MyTransform; + +impl Transform for MyTransform { + fn transform(&mut self, schema: &mut Schema) { + // First, make our change to this schema + schema.insert("my_property".to_string(), serde_json::json!("hello world")); + + // Then apply the transform to any subschemas + transform_subschemas(self, schema); + } +} + +let mut schema = schemars::schema_for!(str); +MyTransform.transform(&mut schema); +``` + +Also, since `Transform` is now implemented for functions that take a single `&mut Schema` argument, you could also define it as a function instead of a struct: + +```rust +fn my_transform(schema: &mut Schema) { + // First, make our change to this schema + schema.insert("my_property".to_string(), serde_json::json!("hello world")); + + // Then apply the transform to any subschemas + transform_subschemas(&mut my_transform, schema); +} + +let mut schema = schemars::schema_for!(str); +my_transform(&mut schema); +// Or equivalently: +// my_transform.transform(&mut schema); +``` + +Finally, you can also use the `RecursiveTransform` newtype to convert a non-recursive `Transform` (i.e. one that does not transform subschemas) into a recursive one, like so: + +```rust +fn my_transform2(schema: &mut Schema) { + schema.insert("my_property".to_string(), serde_json::json!("hello world")); +} + +let mut schema = schemars::schema_for!(str); +RecursiveTransform(my_transform2).transform(&mut schema); +``` + +## Changes to `#[validate(...)]` attributes + +Since [adding support for `#[validate(...)]` attributes](https://graham.cool/schemars/v0/deriving/attributes/#supported-validator-attributes), the [Validator](https://github.com/Keats/validator) crate has made several changes to its supported attributes. Accordingly, Schemars 1.0 has updated its handling of `#[validate(...)]` attributes to match the latest version (currently 0.18.1) of the Validator crate - this removes some attributes, and changes the syntax of others: + +- The `#[validate(phone)]`/`#[schemars(phone)]` attribute is removed. If you want the old behaviour of setting the "format" property on the generated schema, you can use `#[schemars(extend("format = "phone"))]` instead. +- The `#[validate(required_nested)]`/`#[schemars(required_nested)]` attribute is removed. If you want the old behaviour, you can use `#[schemars(required)]` instead. +- The `#[validate(regex = "...")]`/`#[schemars(regex = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use: + + - `#[validate(regex(path = ...)]` + - `#[schemars(regex(pattern = ...)]` + - `#[schemars(pattern(...)]` (Garde-style) + +- Similarly, the `#[validate(contains = "...")]`/`#[schemars(contains = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use: + + - `#[validate(contains(pattern = ...))]` + - `#[schemars(contains(pattern = ...))]` + - `#[schemars(contains(...))]` (Garde-style) + +As an alternative option, Schemars 1.0 also adds support for `#[garde(...)]` attributes used with the [Garde](https://github.com/jprochazk/garde) crate, along with equivalent `#[schemars(...)]` attributes. See [the documentation](https://graham.cool/schemars/deriving/attributes/#supported-validatorgarde-attributes) for a list of all supported attributes. diff --git a/docs/1-deriving.md b/docs/1-deriving.md index 6e37fc87..153ecc06 100644 --- a/docs/1-deriving.md +++ b/docs/1-deriving.md @@ -1,35 +1,9 @@ --- -layout: default title: Deriving JsonSchema -nav_order: 2 +nav_order: 3 has_children: true has_toc: false permalink: /deriving/ --- -# Deriving JsonSchema - -The most important trait in Schemars is `JsonSchema`, and the most important function of that trait is `json_schema(...)` which returns a JSON schema describing the type. Implementing this manually on many types would be slow and error-prone, so Schemars includes a derive macro which can implement that trait for you. Any derived implementation of `JsonSchema` should create a schema that describes the JSON representation of the type if it were to be serialized by serde_json. - -Usually, all you need to do to use it is to add a `#[derive(JsonSchema)]` attribute to your type: -```rust -use schemars::{JsonSchema, schema_for}; - -#[derive(JsonSchema, Debug)] -struct Point { - x: i32, - y: i32, -} - -fn main() { - let schema = schema_for!(Point); - - let serialized = serde_json::to_string(&schema).unwrap(); - println!("{}", serialized); -} -``` - +{% include deriving.md %} diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index 251a6a10..a2159dbb 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -1,5 +1,4 @@ --- -layout: default title: Attributes parent: Deriving JsonSchema nav_order: 1 @@ -9,276 +8,8 @@ permalink: /deriving/attributes/ -# Attributes - -You can add attributes to your types to customize Schemars's derived `JsonSchema` implementation. - -[Serde](https://serde.rs/) allows setting `#[serde(...)]` attributes which change how types are serialized, and Schemars will generally respect these attributes to ensure that generated schemas will match how the type is serialized by serde_json. `#[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. - -[Validator](https://github.com/Keats/validator) allows setting `#[validate(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes. - -
- -TABLE OF CONTENTS - - -1. [Supported Serde Attributes](#supported-serde-attributes) - - [`rename`](#rename) - - [`rename_all`](#rename_all) - - [`tag` / `content` / `untagged`](#tag) - - [`default`](#default) - - [`skip`](#skip) - - [`skip_serializing`](#skip_serializing) - - [`skip_deserializing`](#skip_deserializing) - - [`flatten`](#flatten) - - [`with`](#with) - - [`bound`](#bound) -1. [Supported Validator Attributes](#supported-validator-attributes) - - [`email` / `phone` / `url`](#email-phone-url) - - [`length`](#length) - - [`range`](#range) - - [`regex`](#regex) - - [`contains`](#contains) - - [`required` / `required_nested`](#required) -1. [Other Attributes](#other-attributes) - - [`schema_with`](#schema_with) - - [`title` / `description`](#title-description) - - [`example`](#example) - - [`deprecated`](#deprecated) - - [`crate`](#crate) - - [Doc Comments (`doc`)](#doc) -
- -## Supported Serde Attributes - -
- -

- -`#[serde(rename = "name")]` / `#[schemars(rename = "name")]` -

- -Set on a struct, enum, field or variant to use the given name in the generated schema instead of the Rust name. When used on a struct or enum, the given name will be used as the title for root schemas, and the key within the root's `definitions` property for subschemas. - -If set on a struct or enum with generic type parameters, then the given name may contain them enclosed in curly braces (e.g. `{T}`) and they will be replaced with the concrete type names when the schema is generated. - -Serde docs: [container](https://serde.rs/container-attrs.html#rename) / [variant](https://serde.rs/variant-attrs.html#rename) / [field](https://serde.rs/field-attrs.html#rename) - -

- -`#[serde(rename_all = "...")]` / `#[schemars(rename_all = "...")]` -

- -Set on a struct, enum or variant to rename all fields according to the given case convention (see the Serde docs for details). - -Serde docs: [container](https://serde.rs/container-attrs.html#rename_all) / [variant](https://serde.rs/variant-attrs.html#rename_all) - -

- -`#[serde(tag = "type")]` / `#[schemars(tag = "type")]`
-`#[serde(tag = "t", content = "c")]` / `#[schemars(tag = "t", content = "c")]`
-`#[serde(untagged)]` / `#[schemars(untagged)]` -

- -Set on an enum to generate the schema for the [internally tagged](https://serde.rs/enum-representations.html#internally-tagged), [adjacently tagged](https://serde.rs/enum-representations.html#adjacently-tagged), or [untagged](https://serde.rs/enum-representations.html#untagged) representation of this enum. - -Serde docs: [`tag`](https://serde.rs/container-attrs.html#tag) / [`tag`+`content`](https://serde.rs/container-attrs.html#tag--content) / [`untagged`](https://serde.rs/container-attrs.html#untagged) - -

- -`#[serde(default)]` / `#[schemars(default)]` / `#[serde(default = "path")]` / `#[schemars(default = "path")]` -

- -Set on a struct or field to give fields a default value, which excludes them from the schema's `required` properties. The default will also be set on the field's schema's `default` property, unless it is skipped by a [`skip_serializing_if`](https://serde.rs/field-attrs.html#skip_serializing_if) attribute on the field. Any [`serialize_with`](https://serde.rs/field-attrs.html#serialize_with) or [`with`](https://serde.rs/field-attrs.html#with) attribute set on the field will be used to serialize the default value. - -Serde docs: [container](https://serde.rs/container-attrs.html#default) / [field](https://serde.rs/field-attrs.html#default) - -

- -`#[serde(skip)]` / `#[schemars(skip)]` -

- -Set on a variant or field to prevent it from appearing in any generated schema. - -Serde docs: [variant](https://serde.rs/variant-attrs.html#skip) / [field](https://serde.rs/field-attrs.html#skip) - -

- -`#[serde(skip_serializing)]` / `#[schemars(skip_serializing)]` -

- -Set on a field of a (non-tuple) struct to set the `writeOnly` property on that field's schema. Serde also allows this attribute on variants or tuple struct fields, but this will have no effect on generated schemas. - -Serde docs: [field](https://serde.rs/field-attrs.html#skip_deserializing) - -

- -`#[serde(skip_deserializing)]` / `#[schemars(skip_deserializing)]` -

- -Set on a variant or field. When set on a field of a (non-tuple) struct, that field's schema will have the `readOnly` property set. When set on a variant or tuple struct field Schemars will treat this the same as a [`skip`](#skip) attribute. - -Serde docs: [variant](https://serde.rs/variant-attrs.html#skip_deserializing) / [field](https://serde.rs/field-attrs.html#skip_deserializing) - -

- -`#[serde(flatten)]` / `#[schemars(flatten)]` -

- -Set on a field to include that field's contents as though they belonged to the field's container. - -Serde docs: [field](https://serde.rs/field-attrs.html#flatten) - -

- -`#[serde(with = "Type")]` / `#[schemars(with = "Type")]` -

- -Set on a variant or field to generate its schema as the given type instead of its actual type. Serde allows the `with` attribute to refer to any module path, but Schemars requires this to be an actual type which implements `JsonSchema`. - -If the given type has any required generic type parameters, then they must all be explicitly specified in this attribute. Serde frequently allows you to omit them as it can make use of type inference, but unfortunately this is not possible with Schemars. For example, `with = "Vec::"` will work, but `with = "Vec"` and `with = "Vec::<_>"` will not. - -Serde docs: [variant](https://serde.rs/variant-attrs.html#with) / [field](https://serde.rs/field-attrs.html#with) - -

- -`#[serde(deny_unknown_fields)]` / `#[schemars(deny_unknown_fields)]` -

- -Setting this on a container will set the `additionalProperties` keyword on generated schemas to `false` to show that any extra properties are explicitly disallowed. - -Serde docs: [container](https://serde.rs/container-attrs.html#deny_unknown_fields) - -

- -`#[serde(transparent)]` / `#[schemars(transparent)]` -

- -Set on a newtype struct or a braced struct with one field to make the struct's generated schema exactly the same as that of the single field's. - -Serde docs: [container](https://serde.rs/container-attrs.html#transparent) - -

- -`#[schemars(bound = "...")]` -

- -Where-clause for the JsonSchema impl. This replaces any trait bounds inferred by schemars. Schemars does **not** use trait bounds from `#[serde(bound)]` attributes. - -Serde docs: [container](https://serde.rs/container-attrs.html#bound) - -
- -## Supported Validator Attributes - -
- -

- -`#[validate(email)]` / `#[schemars(email)]`
-`#[validate(phone)]` / `#[schemars(phone)]`
-`#[validate(url)]` / `#[schemars(url)]` -

- -Sets the schema's `format` to `email`/`phone`/`uri`, as appropriate. Only one of these attributes may be present on a single field. - -Validator docs: [email](https://github.com/Keats/validator#email) / [phone](https://github.com/Keats/validator#phone) / [url](https://github.com/Keats/validator#url) - -

- -`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`
-`#[validate(length(equal = 10))]` / `#[schemars(length(equal = 10))]` -

- -Sets the `minLength`/`maxLength` properties for string schemas, or the `minItems`/`maxItems` properties for array schemas. - -Validator docs: [length](https://github.com/Keats/validator#length) - -

- -`#[validate(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]` -

- -Sets the `minimum`/`maximum` properties for number schemas. - -Validator docs: [range](https://github.com/Keats/validator#range) - -

- -`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`
-`#[schemars(regex(pattern = r"^\d+$"))]` -

- -Sets the `pattern` property for string schemas. The `path::to::regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method. - -Providing an inline regex pattern using `regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. - -Validator docs: [regex](https://github.com/Keats/validator#regex) - -

- -`#[validate(contains = "string")]` / `#[schemars(contains = "string")]` -

- -For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped. For object schemas (e.g. when the attribute is set on a HashMap field), includes the value in the `required` property, indicating that the map must contain it as a key. - -Validator docs: [contains](https://github.com/Keats/validator#contains) - -

- -`#[validate(required)]` / `#[schemars(required)]`
-`#[validate(required_nested)]` -

- -When set on an `Option` field, this will create a schemas as though the field were a `T`. - -Validator docs: [required](https://github.com/Keats/validator#required) / [required_nested](https://github.com/Keats/validator#required_nested) - -
- -## Other Attributes - -

- -`#[schemars(schema_with = "some::function")]` -

- -Set on a variant or field to generate this field's schema using the given function. This function must be callable as `fn(&mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema`. - -

- -`#[schemars(title = "Some title", description = "Some description")]` -

- -Set on a container, variant or field to set the generated schema's `title` and/or `description`. If present, these will be used instead of values from any [`doc` comments/attributes](#doc). - -

- -`#[schemars(example = "some::function")]` -

- -Set on a container, variant or field to include the result of the given function in the generated schema's `examples`. The function should take no parameters and can return any type that implements serde's `Serialize` trait - it does not need to return the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. - -

- -`#[deprecated]` -

- -Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rust-2018/the-compiler/an-attribute-for-deprecation.html) attribute on a struct, enum, field or variant to set the generated schema's `deprecated` keyword to `true`. - -

- -`#[schemars(crate = "other_crate::schemars")]` -

- -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. - -

- -Doc Comments (`#[doc = "..."]`) -

- -If a struct, variant or field has any [doc comments](https://doc.rust-lang.org/stable/rust-by-example/meta/doc.html#doc-comments) (or [`doc` attributes](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html)), then these will be used as the generated schema's `description`. If the first line is an ATX-style markdown heading (i.e. it begins with a # character), then it will be used as the schema's `title`, and the remaining lines will be the `description`. +{% include attributes.md %} diff --git a/docs/2-implementing.md b/docs/2-implementing.md index a0183d9d..06dfa0ea 100644 --- a/docs/2-implementing.md +++ b/docs/2-implementing.md @@ -1,46 +1,80 @@ --- -layout: default title: Implementing JsonSchema -nav_order: 3 +nav_order: 4 permalink: /implementing/ --- # Implementing JsonSchema -[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: +[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, one which usually _should_ be implemented, and one which can optionally be implemented: ## schema_name + +```rust +fn schema_name() -> Cow<'static, str>; +``` + +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 `$defs` property for subschemas. + +## schema_id (optional but recommended) + +```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: + ```rust -fn schema_name() -> String; +fn schema_id() -> Cow<'static, str> { + format!("[{}]", T::schema_id()).into() +} ``` -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. +`&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.: -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()) +impl JsonSchema for NonGenericType { + fn schema_name() -> Cow<'static, str> { + // Exclude the module path to make the name in generated schemas clearer. + "NonGenericType".into() + } + + fn schema_id() -> Cow<'static, str> { + // Include the module, in case a type with the same name is in another module/crate + concat!(module_path!(), "::NonGenericType").into() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "object", + "foo": "bar" + }) + } } ``` -`BTreeSet`, `LinkedList`, and similar collection types also use that implementation, since they produce identical JSON schemas so they can be considered the same type. +The default implementation of this function returns `Self::schema_name()`. ## json_schema + ```rust -fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema; +fn json_schema(generator: &mut SchemaGenerator) -> Schema; ``` -This function creates the JSON schema itself. The `gen` argument can be used to check the schema generation settings, or to get schemas for other types. If you do need schemas for other types, you should call the `gen.subschema_for::()` method instead of `::json_schema(gen)`, as `subschema_for` can add `T`'s schema to the root schema's `definitions` so that it does not need to be duplicated when used more than once. +This function creates the JSON schema itself. The `generator` argument can be used to check the schema generation settings, or to get schemas for other types. If you do need schemas for other types, you should call the `generator.subschema_for::()` method instead of `::json_schema(generator)`, as `subschema_for` can add `T`'s schema to the root schema's `$defs` so that it does not need to be duplicated when used more than once. `json_schema` should not return a `$ref` schema. -## is_referenceable (optional) +## always_inline_schema (optional) + ```rust -fn is_referenceable() -> bool; +fn always_inline_schema() -> bool; ``` -If this function returns `true`, then Schemars can re-use the generate schema where possible by adding it to the root schema's `definitions` and having other schemas reference it using the `$ref` keyword. This can greatly simplify schemas that include a particular type multiple times, especially if that type's schema is fairly complex. +If this function returns `false`, then Schemars can re-use the generate schema where possible by adding it to the root schema's `$defs` and having other schemas reference it using the `$ref` keyword. This can greatly simplify schemas that include a particular type multiple times, especially if that type's schema is fairly complex. -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. +Generally, this should return `true` for types with simple schemas (such as primitives). For more complex types, it should return `false`. For recursive types, this **must** return `false` 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 `false` to reduce the chance of someone inadvertently causing infinite cycles with recursive types. diff --git a/docs/3-generating.md b/docs/3-generating.md index f8a284f4..64901697 100644 --- a/docs/3-generating.md +++ b/docs/3-generating.md @@ -1,28 +1,36 @@ --- -layout: default title: Generating Schemas -nav_order: 4 +nav_order: 5 permalink: /generating/ --- # Generating Schemas -The easiest way to generate a schema for a type that implements is to use the [`schema_for!` macro](https://docs.rs/schemars/latest/schemars/macro.schema_for.html), like so: +The easiest way to generate a schema for a type that implements is to use the [`schema_for!` macro](https://docs.rs/schemars/1.0.0--latest/schemars/macro.schema_for.html), like so: + ```rust let my_schema = schema_for!(MyStruct); ``` -This will create a schema that conforms 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. +This will create a schema that conforms to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. + +If you want more control over how the schema is generated, you can use the [`generate` module](https://docs.rs/schemars/1.0.0--latest/schemars/generate/). There are two main types in this module: + +- [`SchemaSettings`](https://docs.rs/schemars/1.0.0--latest/schemars/generate/struct.SchemaSettings.html), which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). +- [`SchemaGenerator`](https://docs.rs/schemars/1.0.0--latest/schemars/generate/struct.SchemaGenerator.html), which manages the generation of a schema document. -If you want more control over how the schema is generated, you can use the [`gen` module](https://docs.rs/schemars/latest/schemars/gen/). There are two main types in this module: -* [`SchemaSettings`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaSettings.html), which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). -* [`SchemaGenerator`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaGenerator.html), which manages the generation of a schema document. +For example, to generate a schema that conforms to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7): + +```rust +let generator = SchemaSettings::draft07().into_generator(); +let my_schema = generator.into_root_schema_for::(); +``` See the API documentation for more info on how to use those types for custom schema generation. ## 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 using the [`schema_for_value!` macro](https://docs.rs/schemars/latest/schemars/macro.schema_for_value.html). 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. +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 using the [`schema_for_value!` macro](https://docs.rs/schemars/1.0.0--latest/schemars/macro.schema_for_value.html). 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 let value = MyStruct { foo = 123 }; diff --git a/docs/4-features.md b/docs/4-features.md index dc3cc850..2ae7dae2 100644 --- a/docs/4-features.md +++ b/docs/4-features.md @@ -1,34 +1,35 @@ --- -layout: default title: Feature Flags -nav_order: 5 +nav_order: 6 permalink: /features/ --- # Feature Flags and Optional Dependencies + +- `std` (enabled by default) - implements `JsonSchema` for types in the rust standard library (`JsonSchema` is still implemented on types in `core` and `alloc`, even when this feature is disabled). Disable this feature to use schemars in `no_std` environments. - `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` +- `preserve_order` - keep the order of struct fields in `Schema` properties +- `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) -- `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) +- `bigdecimal04` - [bigdecimal](https://crates.io/crates/bigdecimal) (^0.4) +- `bytes1` - [bytes](https://crates.io/crates/bytes) (^1.0) +- `chrono04` - [chrono](https://crates.io/crates/chrono) (^0.4) +- `either1` - [either](https://crates.io/crates/either) (^1.3) +- `enumset1` - [enumset](https://crates.io/crates/enumset) (^1.0) +- `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0) +- `rust_decimal1` - [rust_decimal](https://crates.io/crates/rust_decimal) (^1.0) +- `semver1` - [semver](https://crates.io/crates/semver) (^1.0.9) +- `smallvec1` - [smallvec](https://crates.io/crates/smallvec) (^1.0) +- `smol_str02` - [smol_str](https://crates.io/crates/smol_str) (^0.2.1) +- `url2` - [url](https://crates.io/crates/url) (^2.0) +- `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) 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"] } +schemars = { version = "1.0.0-alpha.14", features = ["chrono04"] } ``` diff --git a/docs/5-examples.md b/docs/5-examples.md index 34c04563..b7e38d77 100644 --- a/docs/5-examples.md +++ b/docs/5-examples.md @@ -1,9 +1,8 @@ --- -layout: default title: Examples -nav_order: 6 +nav_order: 7 has_children: true permalink: /examples/ --- -# Examples \ No newline at end of file +# Examples diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..f2d75e69 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,14 @@ +FROM ruby:2.7.4 + +EXPOSE 4000 + +RUN gem update --system 3.4.22 && echo 'url: "http://localhost:4000"' > /_config.localhost.yml + +COPY Gemfile . + +RUN bundle install + +ENV JEKYLL_ENV=docker + +ENTRYPOINT [ "bundle", "exec", "jekyll"] +CMD [ "serve", "--force_polling", "--host", "0.0.0.0", "--config", "./_config.yml,/_config.localhost.yml" ] diff --git a/docs/Gemfile b/docs/Gemfile index fd95e740..8f1a5b53 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -10,7 +10,7 @@ source "https://rubygems.org" # gem "jekyll", "~> 4.0.0" # This is the default theme for new Jekyll sites. You may change this to anything you like. # gem "minima", "~> 2.5" -gem "just-the-docs", "= 0.3.2" +gem "just-the-docs", "= 0.9.0" # If you want to use GitHub Pages, remove the "gem "jekyll"" above and # uncomment the line below. To upgrade, run `bundle update github-pages`. gem "github-pages", group: :jekyll_plugins @@ -24,7 +24,3 @@ install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do gem "tzinfo", "~> 1.2" gem "tzinfo-data" end - -# Performance-booster for watching directories on Windows -gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform? - diff --git a/docs/_config.yml b/docs/_config.yml index ee946f73..3f0afa07 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -27,20 +27,47 @@ url: "https://graham.cool" # the base hostname & protocol for your site, e.g. ht permalink: pretty # Build settings -remote_theme: pmarsceill/just-the-docs@v0.3.2 +remote_theme: pmarsceill/just-the-docs@v0.9.0 markdown: CommonMarkGhPages commonmark: options: ["UNSAFE", "FOOTNOTES"] color_scheme: default aux_links: - 'Schemars on GitHub': - - "https://github.com/GREsau/schemars" - 'Schemars API docs': - - "https://docs.rs/schemars" - 'Schemars on crates.io': - - "https://crates.io/crates/schemars" + 'Schemars on GitHub': + - "https://github.com/GREsau/schemars" + 'Schemars API docs': + - "https://docs.rs/schemars" + 'Schemars on crates.io': + - "https://crates.io/crates/schemars" +collections: + v0: + permalink: "/:collection/:path/" + output: true + +just_the_docs: + collections: + v0: + name: v0.8 + nav_fold: true + +defaults: + - scope: + path: "" + type: v0 + values: + layout: v0 + - scope: + path: "" + values: + layout: v1 + +callouts_level: quiet +callouts: + info: + color: blue + opacity: 0.1 # Exclude from processing. # The following items will not be processed, by default. # Any item listed under the `exclude:` key here will be automatically added to diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md new file mode 100644 index 00000000..37f6833b --- /dev/null +++ b/docs/_includes/attributes.md @@ -0,0 +1,367 @@ +# Attributes + +You can add attributes to your types to customize Schemars's derived `JsonSchema` implementation. + +[Serde](https://serde.rs/) allows setting `#[serde(...)]` attributes which change how types are serialized, and Schemars will generally respect these attributes to ensure that generated schemas will match how the type is serialized by serde_json. `#[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. + +[Validator](https://github.com/Keats/validator) and [Garde](https://github.com/jprochazk/garde) allow setting `#[validate(...)]`/`#[garde(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes. + +
+ +TABLE OF CONTENTS + + +1. [Supported Serde Attributes](#supported-serde-attributes) + - [`rename`](#rename) + - [`rename_all`](#rename_all) + - [`rename_all_fields`](#rename_all_fields) + - [`tag` / `content` / `untagged`](#tag) + - [`default`](#default) + - [`skip`](#skip) + - [`skip_serializing`](#skip_serializing) + - [`skip_deserializing`](#skip_deserializing) + - [`flatten`](#flatten) + - [`with`](#with) + - [`bound`](#bound) +1. [Supported Validator/Garde Attributes](#supported-validatorgarde-attributes) + - [`email` / `url` / `ip` / `ipv4` / `ipv6`](#formats) + - [`length`](#length) + - [`range`](#range) + - [`regex` / `pattern`](#regex) + - [`contains`](#contains) + - [`required`](#required) + - [`inner`](#inner) +1. [Other Attributes](#other-attributes) + - [`schema_with`](#schema_with) + - [`title` / `description`](#title-description) + - [`example`](#example) + - [`deprecated`](#deprecated) + - [`crate`](#crate) + - [`extend`](#extend) + - [`transform`](#transform) + - [Doc Comments (`doc`)](#doc) + +
+ +## Supported Serde Attributes + +
+ +

+ +`#[serde(rename = "name")]` / `#[schemars(rename = "name")]` + +

+ +Set on a struct, enum, field or variant to use the given name in the generated schema instead of the Rust name. When used on a struct or enum, the given name will be used as the title for root schemas, and the key within the root's `$defs` property for subschemas. + +If set on a struct or enum with generic type parameters, then the given name may contain them enclosed in curly braces (e.g. `{T}`) and they will be replaced with the concrete type names when the schema is generated. + +Serde docs: [container](https://serde.rs/container-attrs.html#rename) / [variant](https://serde.rs/variant-attrs.html#rename) / [field](https://serde.rs/field-attrs.html#rename) + +

+ +`#[serde(rename_all = "...")]` / `#[schemars(rename_all = "...")]` + +

+ +Set on a struct, enum or variant to rename all fields according to the given case convention (see the Serde docs for details). + +Serde docs: [container](https://serde.rs/container-attrs.html#rename_all) / [variant](https://serde.rs/variant-attrs.html#rename_all) + +

+ +`#[serde(rename_all_fields = "...")]` / `#[schemars(rename_all_fields = "...")]` + +

+ +Set on an enum to rename all fields of all struct-style variants according to the given case convention (see the Serde docs for details). + +Serde docs: [container](https://serde.rs/container-attrs.html#rename_all) + +

+ +`#[serde(tag = "type")]` / `#[schemars(tag = "type")]`
+`#[serde(tag = "t", content = "c")]` / `#[schemars(tag = "t", content = "c")]`
+`#[serde(untagged)]` / `#[schemars(untagged)]` + +

+ +Set on an enum to generate the schema for the [internally tagged](https://serde.rs/enum-representations.html#internally-tagged), [adjacently tagged](https://serde.rs/enum-representations.html#adjacently-tagged), or [untagged](https://serde.rs/enum-representations.html#untagged) representation of this enum. + +Serde docs: [`tag`](https://serde.rs/container-attrs.html#tag) / [`tag`+`content`](https://serde.rs/container-attrs.html#tag--content) / [`untagged`](https://serde.rs/container-attrs.html#untagged) + +

+ +`#[serde(default)]` / `#[schemars(default)]` / `#[serde(default = "path")]` / `#[schemars(default = "path")]` + +

+ +Set on a struct or field to give fields a default value, which excludes them from the schema's `required` properties. The default will also be set on the field's schema's `default` property, unless it is skipped by a [`skip_serializing_if`](https://serde.rs/field-attrs.html#skip_serializing_if) attribute on the field. Any [`serialize_with`](https://serde.rs/field-attrs.html#serialize_with) or [`with`](https://serde.rs/field-attrs.html#with) attribute set on the field will be used to serialize the default value. + +Serde docs: [container](https://serde.rs/container-attrs.html#default) / [field](https://serde.rs/field-attrs.html#default) + +

+ +`#[serde(skip)]` / `#[schemars(skip)]` + +

+ +Set on a variant or field to prevent it from appearing in any generated schema. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#skip) / [field](https://serde.rs/field-attrs.html#skip) + +

+ +`#[serde(skip_serializing)]` / `#[schemars(skip_serializing)]` + +

+ +Set on a field of a (non-tuple) struct to set the `writeOnly` property on that field's schema. Serde also allows this attribute on variants or tuple struct fields, but this will have no effect on generated schemas. + +Serde docs: [field](https://serde.rs/field-attrs.html#skip_deserializing) + +

+ +`#[serde(skip_deserializing)]` / `#[schemars(skip_deserializing)]` + +

+ +Set on a variant or field. When set on a field of a (non-tuple) struct, that field's schema will have the `readOnly` property set. When set on a variant or tuple struct field Schemars will treat this the same as a [`skip`](#skip) attribute. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#skip_deserializing) / [field](https://serde.rs/field-attrs.html#skip_deserializing) + +

+ +`#[serde(flatten)]` / `#[schemars(flatten)]` + +

+ +Set on a field to include that field's contents as though they belonged to the field's container. + +Serde docs: [field](https://serde.rs/field-attrs.html#flatten) + +

+ +`#[serde(with = "Type")]` / `#[schemars(with = "Type")]` + +

+ +Set on a variant or field to generate its schema as the given type instead of its actual type. Serde allows the `with` attribute to refer to any module path, but Schemars requires this to be an actual type which implements `JsonSchema`. + +If the given type has any required generic type parameters, then they must all be explicitly specified in this attribute. Serde frequently allows you to omit them as it can make use of type inference, but unfortunately this is not possible with Schemars. For example, `with = "Vec::"` will work, but `with = "Vec"` and `with = "Vec::<_>"` will not. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#with) / [field](https://serde.rs/field-attrs.html#with) + +

+ +`#[serde(deny_unknown_fields)]` / `#[schemars(deny_unknown_fields)]` + +

+ +Setting this on a container will set the `additionalProperties` keyword on generated schemas to `false` to show that any extra properties are explicitly disallowed. + +Serde docs: [container](https://serde.rs/container-attrs.html#deny_unknown_fields) + +

+ +`#[serde(transparent)]` / `#[schemars(transparent)]` + +

+ +Set on a newtype struct or a braced struct with one field to make the struct's generated schema exactly the same as that of the single field's. + +Serde docs: [container](https://serde.rs/container-attrs.html#transparent) + +

+ +`#[schemars(bound = "...")]` + +

+ +Where-clause for the JsonSchema impl. This replaces any trait bounds inferred by schemars. Schemars does **not** use trait bounds from `#[serde(bound)]` attributes. + +Serde docs: [container](https://serde.rs/container-attrs.html#bound) + +
+ +## Supported Validator/Garde Attributes + +
+ +

+ +`#[validate(email)]` / `#[garde(email)]` / `#[schemars(email)]`
+`#[validate(url)]` / `#[garde(url)]`/ `#[schemars(url)]`
+`#[garde(ip)]`/ `#[schemars(ip)]`
+`#[garde(ipv4)]`/ `#[schemars(ipv4)]`
+`#[garde(ipv6)]`/ `#[schemars(ip)v6]`
+ +

+ +Sets the schema's `format` to `email`/`uri`/`ip`/`ipv4`/`ipv6`, as appropriate. Only one of these attributes may be present on a single field. + +Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url) + +

+ +`#[validate(length(min = 1, max = 10))]` / `#[garde(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`
+`#[validate(length(equal = 10))]` / `#[garde(length(equal = 10))]` / `#[schemars(length(equal = 10))]` + +

+ +Sets the `minLength`/`maxLength` properties for string schemas, or the `minItems`/`maxItems` properties for array schemas. + +Validator docs: [length](https://github.com/Keats/validator#length) + +

+ +`#[validate(range(min = 1, max = 10))]` / `#[garde(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]` + +

+ +Sets the `minimum`/`maximum` properties for number schemas. + +Validator docs: [range](https://github.com/Keats/validator#range) + +

+ +`#[validate(regex(path = *static_regex)]`
+`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
+`#[garde(pattern(r"^\d+$")]` / `#[schemars(pattern(r"^\d+$")]`/ `#[schemars(pattern(*static_regex)]` + +

+ +Sets the `pattern` property for string schemas. The `static_regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method. + +`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form (or the Garde-style `pattern` attribute), you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path = ...` form is not allowed in a `#[schemars(...)]` attribute. + +Validator docs: [regex](https://github.com/Keats/validator#regex) + +

+ +`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
+`#[garde(contains("string"))]` / `#[schemars(contains("string"))]` + +

+ +For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped. + +Validator docs: [contains](https://github.com/Keats/validator#contains) + +

+ +`#[validate(required)]` / `#[garde(required)]` / `#[schemars(required)]`
+ +

+ +When set on an `Option` field, this will create a schemas as though the field were a `T`. + +Validator docs: [required](https://github.com/Keats/validator#required) + +
+ +

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

+ +Sets properties specified by [validation attributes](#supported-validatorgarde-attributes) on items of an array schema. For example: + +```rust +struct Struct { + #[schemars(inner(url, pattern("^https://")))] + urls: Vec, +} +``` + +Garde docs: [Inner type validation](https://github.com/jprochazk/garde?tab=readme-ov-file#inner-type-validation) + +## Other Attributes + +

+ +`#[schemars(schema_with = "some::function")]` + +

+ +Set on a variant or field to generate this field's schema using the given function. This function must be callable as `fn(&mut schemars::SchemaGenerator) -> schemars::schema::Schema`. + +

+ +`#[schemars(title = "Some title", description = "Some description")]` + +

+ +Set on a container, variant or field to set the generated schema's `title` and/or `description`. If present, these will be used instead of values from any [`doc` comments/attributes](#doc). + +

+ +`#[schemars(example = "some::function")]` + +

+ +Set on a container, variant or field to include the result of the given function in the generated schema's `examples`. The function should take no parameters and can return any type that implements serde's `Serialize` trait - it does not need to return the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. + +To use the result of arbitrary expressions as examples, you can instead use the [`extend`](#extend) attribute, e.g. `[schemars(extend("examples" = ["example string"]))]`. + +

+ +`#[deprecated]` + +

+ +Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rust-2018/the-compiler/an-attribute-for-deprecation.html) attribute on a struct, enum, field or variant to set the generated schema's `deprecated` keyword to `true`. + +

+ +`#[schemars(crate = "other_crate::schemars")]` + +

+ +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(extend("key" = value))]` + +

+ +Set on a container, variant or field to add properties (or replace existing properties) in a generated schema. This can contain multiple key/value pairs and/or be specified multiple times, as long as each key is unique. + +The key must be a quoted string, and the value can be any expression that produces a type implementing `serde::Serialize`. The value can also be a JSON literal which can interpolate other values. + +```plaintext +#[derive(JsonSchema)] +#[schemars(extend("simple" = "string value", "complex" = {"array": [1, 2, 3]}))] +struct Struct; +``` + +

+ +`#[schemars(transform = some::transform)]` + +

+ +Set on a container, variant or field to run a `schemars::transform::Transform` against the generated schema. This can be specified multiple times to run multiple transforms. + +The `Transform` trait is implemented on functions with the signature `fn(&mut Schema) -> ()`, allowing you to do this: + +```rust +fn my_transform(schema: &mut Schema) { + todo!() +} + +#[derive(JsonSchema)] +#[schemars(transform = my_transform)] +struct Struct; +``` + +

+ +Doc Comments (`#[doc = "..."]`) + +

+ +If a struct, variant or field has any [doc comments](https://doc.rust-lang.org/stable/rust-by-example/meta/doc.html#doc-comments) (or [`doc` attributes](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html)), then these will be used as the generated schema's `description`. If the first line is an ATX-style markdown heading (i.e. it begins with a # character), then it will be used as the schema's `title`, and the remaining lines will be the `description`. diff --git a/docs/_includes/deriving.md b/docs/_includes/deriving.md new file mode 100644 index 00000000..0d2c4844 --- /dev/null +++ b/docs/_includes/deriving.md @@ -0,0 +1,28 @@ +# Deriving JsonSchema + +The most important trait in Schemars is `JsonSchema`, and the most important function of that trait is `json_schema(...)` which returns a JSON schema describing the type. Implementing this manually on many types would be slow and error-prone, so Schemars includes a derive macro which can implement that trait for you. Any derived implementation of `JsonSchema` should create a schema that describes the JSON representation of the type if it were to be serialized by serde_json. + +Usually, all you need to do to use it is to add a `#[derive(JsonSchema)]` attribute to your type: + +```rust +use schemars::{JsonSchema, schema_for}; + +#[derive(JsonSchema, Debug)] +struct Point { + x: i32, + y: i32, +} + +fn main() { + let schema = schema_for!(Point); + + let serialized = serde_json::to_string(&schema).unwrap(); + println!("{}", serialized); +} +``` + + diff --git a/docs/_includes/example_v0.md b/docs/_includes/example_v0.md new file mode 100644 index 00000000..88685242 --- /dev/null +++ b/docs/_includes/example_v0.md @@ -0,0 +1,14 @@ +{% capture input %}examples/{{ include.name }}.rs{% endcapture %} +{% capture output %}examples/{{ include.name }}.schema.json{% endcapture %} + +```rust +{% include {{ input }} %} +``` + +
+Click to see the output JSON schema... + +```json +{% include {{ output }} -%} +``` +
diff --git a/docs/_includes/examples/custom_serialization.rs b/docs/_includes/examples/custom_serialization.rs index 53c78fa6..8e2a695a 100644 --- a/docs/_includes/examples/custom_serialization.rs +++ b/docs/_includes/examples/custom_serialization.rs @@ -1,5 +1,4 @@ -use schemars::schema::{Schema, SchemaObject}; -use schemars::{gen::SchemaGenerator, schema_for, JsonSchema}; +use schemars::{schema_for, JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; // `int_as_string` and `bool_as_string` use the schema for `String`. @@ -20,10 +19,12 @@ pub struct MyStruct { pub bool_normal: bool, } -fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = ::json_schema(gen).into(); - schema.format = Some("boolean".to_owned()); - schema.into() +fn make_custom_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = String::json_schema(generator); + schema + .ensure_object() + .insert("format".into(), "boolean".into()); + schema } fn eight() -> i32 { diff --git a/docs/_includes/examples/custom_serialization.schema.json b/docs/_includes/examples/custom_serialization.schema.json index 42fda993..1aa09420 100644 --- a/docs/_includes/examples/custom_serialization.schema.json +++ b/docs/_includes/examples/custom_serialization.schema.json @@ -1,25 +1,25 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", "properties": { "bool_as_string": { - "default": "false", "type": "string", - "format": "boolean" + "format": "boolean", + "default": "false" }, "bool_normal": { - "default": false, - "type": "boolean" + "type": "boolean", + "default": false }, "int_as_string": { - "default": "8", - "type": "string" + "type": "string", + "default": "8" }, "int_normal": { - "default": 8, "type": "integer", - "format": "int32" + "format": "int32", + "default": 8 } } } diff --git a/docs/_includes/examples/custom_settings.rs b/docs/_includes/examples/custom_settings.rs index 335daf3c..7858720f 100644 --- a/docs/_includes/examples/custom_settings.rs +++ b/docs/_includes/examples/custom_settings.rs @@ -1,4 +1,4 @@ -use schemars::{gen::SchemaSettings, JsonSchema}; +use schemars::{generate::SchemaSettings, JsonSchema}; #[derive(JsonSchema)] pub struct MyStruct { @@ -18,7 +18,7 @@ fn main() { s.option_nullable = true; s.option_add_null_type = false; }); - let gen = settings.into_generator(); - let schema = gen.into_root_schema_for::(); + let generator = settings.into_generator(); + let schema = generator.into_root_schema_for::(); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } diff --git a/docs/_includes/examples/custom_settings.schema.json b/docs/_includes/examples/custom_settings.schema.json index 12ac7d59..8da43482 100644 --- a/docs/_includes/examples/custom_settings.schema.json +++ b/docs/_includes/examples/custom_settings.schema.json @@ -2,10 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "type": "boolean" @@ -23,32 +19,30 @@ "nullable": true } }, + "required": [ + "my_int", + "my_bool" + ], "definitions": { "MyEnum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -57,10 +51,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/docs/_includes/examples/doc_comments.schema.json b/docs/_includes/examples/doc_comments.schema.json index 121cdb42..a3aa7670 100644 --- a/docs/_includes/examples/doc_comments.schema.json +++ b/docs/_includes/examples/doc_comments.schema.json @@ -1,12 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "My Amazing Struct", - "description": "This struct shows off generating a schema with a custom title and description.", + "description": "This struct shows off generating a schema with\n a custom title and description.", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "description": "This bool has a description, but no title.", @@ -22,7 +18,7 @@ "description": "This enum might be set, or it might not.", "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" @@ -30,35 +26,33 @@ ] } }, - "definitions": { + "required": [ + "my_int", + "my_bool" + ], + "$defs": { "MyEnum": { "title": "My Amazing Enum", "oneOf": [ { "description": "A wrapper around a `String`", "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { - "description": "A struct-like enum variant which contains some floats", + "description": "A struct-like enum variant which contains\n some floats", "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "description": "The floats themselves", @@ -68,10 +62,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/docs/_includes/examples/enum_repr.schema.json b/docs/_includes/examples/enum_repr.schema.json index 04841b7d..22d2233f 100644 --- a/docs/_includes/examples/enum_repr.schema.json +++ b/docs/_includes/examples/enum_repr.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "SmallPrime", "type": "integer", "enum": [ diff --git a/docs/_includes/examples/from_value.schema.json b/docs/_includes/examples/from_value.schema.json index 4ba77351..64bad01c 100644 --- a/docs/_includes/examples/from_value.schema.json +++ b/docs/_includes/examples/from_value.schema.json @@ -1,15 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", - "examples": [ - { - "my_bool": true, - "my_int": 123, - "my_nullable_enum": { - "StringNewType": "foo" - } - } - ], "type": "object", "properties": { "my_bool": { @@ -19,5 +10,14 @@ "type": "integer" }, "my_nullable_enum": true - } + }, + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ] } diff --git a/docs/_includes/examples/main.schema.json b/docs/_includes/examples/main.schema.json index ddbd9d33..5eab9093 100644 --- a/docs/_includes/examples/main.schema.json +++ b/docs/_includes/examples/main.schema.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "type": "boolean" @@ -17,7 +13,7 @@ "my_nullable_enum": { "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" @@ -25,32 +21,30 @@ ] } }, - "definitions": { + "required": [ + "my_int", + "my_bool" + ], + "$defs": { "MyEnum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -59,10 +53,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/docs/_includes/examples/remote_derive.schema.json b/docs/_includes/examples/remote_derive.schema.json index e5841778..df697510 100644 --- a/docs/_includes/examples/remote_derive.schema.json +++ b/docs/_includes/examples/remote_derive.schema.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Process", "type": "object", - "required": [ - "command_line", - "durations", - "wall_time" - ], "properties": { "command_line": { "type": "string" @@ -14,20 +9,21 @@ "durations": { "type": "array", "items": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" } }, "wall_time": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" } }, - "definitions": { + "required": [ + "command_line", + "wall_time", + "durations" + ], + "$defs": { "Duration": { "type": "object", - "required": [ - "nanos", - "secs" - ], "properties": { "nanos": { "type": "integer", @@ -37,7 +33,11 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "secs", + "nanos" + ] } } } diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs index cd69b527..21c3bdcf 100644 --- a/docs/_includes/examples/schemars_attrs.rs +++ b/docs/_includes/examples/schemars_attrs.rs @@ -1,27 +1,33 @@ -use schemars::{schema_for, JsonSchema}; +use schemars::{schema_for, JsonSchema, Schema}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, JsonSchema)] -#[schemars(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename_all = "camelCase", deny_unknown_fields, extend("x-customProperty" = "example"))] pub struct MyStruct { #[serde(rename = "thisIsOverridden")] - #[schemars(rename = "myNumber", range(min = 1, max = 10))] + #[schemars(rename = "myNumber", range(min = 1, max = 10), transform = remove_format)] pub my_int: i32, 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)] #[schemars(untagged)] pub enum MyEnum { - StringNewType(#[schemars(phone)] String), + StringNewType(#[schemars(email)] String), StructVariant { #[schemars(length(min = 1, max = 100))] floats: Vec, }, } +fn remove_format(schema: &mut Schema) { + schema.remove("format"); +} + fn main() { let schema = schema_for!(MyStruct); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json index 958cb6bb..19a613c2 100644 --- a/docs/_includes/examples/schemars_attrs.schema.json +++ b/docs/_includes/examples/schemars_attrs.schema.json @@ -1,46 +1,51 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { - "default": null, "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" } - ] + ], + "default": null }, "myNumber": { "type": "integer", - "format": "int32", - "maximum": 10.0, - "minimum": 1.0 + "maximum": 10, + "minimum": 1 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, - "definitions": { + "required": [ + "myNumber", + "myBool", + "myVecStr" + ], + "x-customProperty": "example", + "$defs": { "MyEnum": { "anyOf": [ { "type": "string", - "format": "phone" + "format": "email" }, { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -51,7 +56,10 @@ "maxItems": 100, "minItems": 1 } - } + }, + "required": [ + "floats" + ] } ] } diff --git a/docs/_includes/examples/serde_attrs.schema.json b/docs/_includes/examples/serde_attrs.schema.json index d0441932..81798994 100644 --- a/docs/_includes/examples/serde_attrs.schema.json +++ b/docs/_includes/examples/serde_attrs.schema.json @@ -1,25 +1,21 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { - "default": null, "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" } - ] + ], + "default": null }, "myNumber": { "type": "integer", @@ -27,7 +23,11 @@ } }, "additionalProperties": false, - "definitions": { + "required": [ + "myNumber", + "myBool" + ], + "$defs": { "MyEnum": { "anyOf": [ { @@ -35,9 +35,6 @@ }, { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -46,7 +43,10 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } ] } diff --git a/docs/_includes/examples/validate.rs b/docs/_includes/examples/validate.rs index 41169765..70fa4a2b 100644 --- a/docs/_includes/examples/validate.rs +++ b/docs/_includes/examples/validate.rs @@ -11,7 +11,7 @@ pub struct MyStruct { #[derive(JsonSchema)] pub enum MyEnum { - StringNewType(#[validate(phone)] String), + StringNewType(#[validate(email)] String), StructVariant { #[validate(length(min = 1, max = 100))] floats: Vec, diff --git a/docs/_includes/examples/validate.schema.json b/docs/_includes/examples/validate.schema.json index 1e45a969..c7728b8e 100644 --- a/docs/_includes/examples/validate.schema.json +++ b/docs/_includes/examples/validate.schema.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int", - "my_nullable_enum" - ], "properties": { "my_bool": { "type": "boolean" @@ -14,35 +9,29 @@ "my_int": { "type": "integer", "format": "int32", - "maximum": 10.0, - "minimum": 1.0 + "maximum": 10, + "minimum": 1 }, "my_nullable_enum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string", - "format": "phone" + "format": "email" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -53,12 +42,23 @@ "maxItems": 100, "minItems": 1 } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } - } + }, + "required": [ + "my_int", + "my_bool", + "my_nullable_enum" + ] } diff --git a/docs/_includes/examples_v0/custom_serialization.rs b/docs/_includes/examples_v0/custom_serialization.rs new file mode 100644 index 00000000..53c78fa6 --- /dev/null +++ b/docs/_includes/examples_v0/custom_serialization.rs @@ -0,0 +1,60 @@ +use schemars::schema::{Schema, SchemaObject}; +use schemars::{gen::SchemaGenerator, schema_for, JsonSchema}; +use serde::{Deserialize, Serialize}; + +// `int_as_string` and `bool_as_string` use the schema for `String`. +#[derive(Default, Deserialize, Serialize, JsonSchema)] +pub struct MyStruct { + #[serde(default = "eight", with = "as_string")] + #[schemars(with = "String")] + pub int_as_string: i32, + + #[serde(default = "eight")] + pub int_normal: i32, + + #[serde(default, with = "as_string")] + #[schemars(schema_with = "make_custom_schema")] + pub bool_as_string: bool, + + #[serde(default)] + pub bool_normal: bool, +} + +fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema { + let mut schema: SchemaObject = ::json_schema(gen).into(); + schema.format = Some("boolean".to_owned()); + schema.into() +} + +fn eight() -> i32 { + 8 +} + +// This module serializes values as strings +mod as_string { + use serde::{de::Error, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: std::fmt::Display, + S: Serializer, + { + serializer.collect_str(value) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: std::str::FromStr, + D: Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + string + .parse() + .map_err(|_| D::Error::custom("Input was not valid")) + } +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/custom_serialization.schema.json b/docs/_includes/examples_v0/custom_serialization.schema.json new file mode 100644 index 00000000..42fda993 --- /dev/null +++ b/docs/_includes/examples_v0/custom_serialization.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "type": "object", + "properties": { + "bool_as_string": { + "default": "false", + "type": "string", + "format": "boolean" + }, + "bool_normal": { + "default": false, + "type": "boolean" + }, + "int_as_string": { + "default": "8", + "type": "string" + }, + "int_normal": { + "default": 8, + "type": "integer", + "format": "int32" + } + } +} diff --git a/docs/_includes/examples_v0/custom_settings.rs b/docs/_includes/examples_v0/custom_settings.rs new file mode 100644 index 00000000..335daf3c --- /dev/null +++ b/docs/_includes/examples_v0/custom_settings.rs @@ -0,0 +1,24 @@ +use schemars::{gen::SchemaSettings, 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 }, +} + +fn main() { + let settings = SchemaSettings::draft07().with(|s| { + s.option_nullable = true; + s.option_add_null_type = false; + }); + let gen = settings.into_generator(); + let schema = gen.into_root_schema_for::(); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/custom_settings.schema.json b/docs/_includes/examples_v0/custom_settings.schema.json new file mode 100644 index 00000000..12ac7d59 --- /dev/null +++ b/docs/_includes/examples_v0/custom_settings.schema.json @@ -0,0 +1,68 @@ +{ + "$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": { + "allOf": [ + { + "$ref": "#/definitions/MyEnum" + } + ], + "nullable": true + } + }, + "definitions": { + "MyEnum": { + "oneOf": [ + { + "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 + } + ] + } + } +} diff --git a/docs/_includes/examples_v0/doc_comments.rs b/docs/_includes/examples_v0/doc_comments.rs new file mode 100644 index 00000000..c3faa4f1 --- /dev/null +++ b/docs/_includes/examples_v0/doc_comments.rs @@ -0,0 +1,33 @@ +use schemars::{schema_for, JsonSchema}; + +/// # My Amazing Struct +/// This struct shows off generating a schema with +/// a custom title and description. +#[derive(JsonSchema)] +pub struct MyStruct { + /// # My Amazing Integer + pub my_int: i32, + /// This bool has a description, but no title. + pub my_bool: bool, + /// # A Nullable Enum + /// This enum might be set, or it might not. + pub my_nullable_enum: Option, +} + +/// # My Amazing Enum +#[derive(JsonSchema)] +pub enum MyEnum { + /// A wrapper around a `String` + StringNewType(String), + /// A struct-like enum variant which contains + /// some floats + StructVariant { + /// The floats themselves + floats: Vec, + }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/doc_comments.schema.json b/docs/_includes/examples_v0/doc_comments.schema.json new file mode 100644 index 00000000..121cdb42 --- /dev/null +++ b/docs/_includes/examples_v0/doc_comments.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "My Amazing Struct", + "description": "This struct shows off generating a schema with a custom title and description.", + "type": "object", + "required": [ + "my_bool", + "my_int" + ], + "properties": { + "my_bool": { + "description": "This bool has a description, but no title.", + "type": "boolean" + }, + "my_int": { + "title": "My Amazing Integer", + "type": "integer", + "format": "int32" + }, + "my_nullable_enum": { + "title": "A Nullable Enum", + "description": "This enum might be set, or it might not.", + "anyOf": [ + { + "$ref": "#/definitions/MyEnum" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "MyEnum": { + "title": "My Amazing Enum", + "oneOf": [ + { + "description": "A wrapper around a `String`", + "type": "object", + "required": [ + "StringNewType" + ], + "properties": { + "StringNewType": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A struct-like enum variant which contains some floats", + "type": "object", + "required": [ + "StructVariant" + ], + "properties": { + "StructVariant": { + "type": "object", + "required": [ + "floats" + ], + "properties": { + "floats": { + "description": "The floats themselves", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/docs/_includes/examples_v0/enum_repr.rs b/docs/_includes/examples_v0/enum_repr.rs new file mode 100644 index 00000000..3b0df974 --- /dev/null +++ b/docs/_includes/examples_v0/enum_repr.rs @@ -0,0 +1,15 @@ +use schemars::{schema_for, JsonSchema_repr}; + +#[derive(JsonSchema_repr)] +#[repr(u8)] +enum SmallPrime { + Two = 2, + Three = 3, + Five = 5, + Seven = 7, +} + +fn main() { + let schema = schema_for!(SmallPrime); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/enum_repr.schema.json b/docs/_includes/examples_v0/enum_repr.schema.json new file mode 100644 index 00000000..04841b7d --- /dev/null +++ b/docs/_includes/examples_v0/enum_repr.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SmallPrime", + "type": "integer", + "enum": [ + 2, + 3, + 5, + 7 + ] +} diff --git a/docs/_includes/examples_v0/from_value.rs b/docs/_includes/examples_v0/from_value.rs new file mode 100644 index 00000000..9d509613 --- /dev/null +++ b/docs/_includes/examples_v0/from_value.rs @@ -0,0 +1,24 @@ +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 }, +} + +fn main() { + 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()); +} diff --git a/docs/_includes/examples_v0/from_value.schema.json b/docs/_includes/examples_v0/from_value.schema.json new file mode 100644 index 00000000..4ba77351 --- /dev/null +++ b/docs/_includes/examples_v0/from_value.schema.json @@ -0,0 +1,23 @@ +{ + "$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 + } +} diff --git a/docs/_includes/examples_v0/main.rs b/docs/_includes/examples_v0/main.rs new file mode 100644 index 00000000..58db8b92 --- /dev/null +++ b/docs/_includes/examples_v0/main.rs @@ -0,0 +1,19 @@ +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 }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/main.schema.json b/docs/_includes/examples_v0/main.schema.json new file mode 100644 index 00000000..ddbd9d33 --- /dev/null +++ b/docs/_includes/examples_v0/main.schema.json @@ -0,0 +1,70 @@ +{ + "$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": { + "oneOf": [ + { + "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 + } + ] + } + } +} diff --git a/docs/_includes/examples_v0/remote_derive.rs b/docs/_includes/examples_v0/remote_derive.rs new file mode 100644 index 00000000..db01d8d7 --- /dev/null +++ b/docs/_includes/examples_v0/remote_derive.rs @@ -0,0 +1,42 @@ +// Pretend that this is somebody else's crate, not a module. +mod other_crate { + // Neither Schemars nor the other crate provides a JsonSchema impl + // for this struct. + pub struct Duration { + pub secs: i64, + pub nanos: i32, + } +} + +//////////////////////////////////////////////////////////////////////////////// + +use other_crate::Duration; +use schemars::{schema_for, JsonSchema}; + +// This is just a copy of the remote data structure that Schemars can use to +// create a suitable JsonSchema impl. +#[derive(JsonSchema)] +#[serde(remote = "Duration")] +pub struct DurationDef { + pub secs: i64, + pub nanos: i32, +} + +// Now the remote type can be used almost like it had its own JsonSchema impl +// all along. The `with` attribute gives the path to the definition for the +// remote type. Note that the real type of the field is the remote type, not +// the definition type. +#[derive(JsonSchema)] +pub struct Process { + pub command_line: String, + #[serde(with = "DurationDef")] + pub wall_time: Duration, + // Generic types must be explicitly specified with turbofix `::<>` syntax. + #[serde(with = "Vec::")] + pub durations: Vec, +} + +fn main() { + let schema = schema_for!(Process); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/remote_derive.schema.json b/docs/_includes/examples_v0/remote_derive.schema.json new file mode 100644 index 00000000..e5841778 --- /dev/null +++ b/docs/_includes/examples_v0/remote_derive.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Process", + "type": "object", + "required": [ + "command_line", + "durations", + "wall_time" + ], + "properties": { + "command_line": { + "type": "string" + }, + "durations": { + "type": "array", + "items": { + "$ref": "#/definitions/Duration" + } + }, + "wall_time": { + "$ref": "#/definitions/Duration" + } + }, + "definitions": { + "Duration": { + "type": "object", + "required": [ + "nanos", + "secs" + ], + "properties": { + "nanos": { + "type": "integer", + "format": "int32" + }, + "secs": { + "type": "integer", + "format": "int64" + } + } + } + } +} diff --git a/docs/_includes/examples_v0/schemars_attrs.rs b/docs/_includes/examples_v0/schemars_attrs.rs new file mode 100644 index 00000000..c8c1412f --- /dev/null +++ b/docs/_includes/examples_v0/schemars_attrs.rs @@ -0,0 +1,30 @@ +use schemars::{schema_for, JsonSchema}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, JsonSchema)] +#[schemars(rename_all = "camelCase", deny_unknown_fields)] +pub struct MyStruct { + #[serde(rename = "thisIsOverridden")] + #[schemars(rename = "myNumber", range(min = 1, max = 10))] + pub my_int: i32, + 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)] +#[schemars(untagged)] +pub enum MyEnum { + StringNewType(#[schemars(email)] String), + StructVariant { + #[schemars(length(min = 1, max = 100))] + floats: Vec, + }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/schemars_attrs.schema.json b/docs/_includes/examples_v0/schemars_attrs.schema.json new file mode 100644 index 00000000..9a6a22a6 --- /dev/null +++ b/docs/_includes/examples_v0/schemars_attrs.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "type": "object", + "required": [ + "myBool", + "myNumber", + "myVecStr" + ], + "properties": { + "myBool": { + "type": "boolean" + }, + "myNullableEnum": { + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/MyEnum" + }, + { + "type": "null" + } + ] + }, + "myNumber": { + "type": "integer", + "format": "int32", + "maximum": 10.0, + "minimum": 1.0 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } + } + }, + "additionalProperties": false, + "definitions": { + "MyEnum": { + "anyOf": [ + { + "type": "string", + "format": "phone" + }, + { + "type": "object", + "required": [ + "floats" + ], + "properties": { + "floats": { + "type": "array", + "items": { + "type": "number", + "format": "float" + }, + "maxItems": 100, + "minItems": 1 + } + } + } + ] + } + } +} diff --git a/docs/_includes/examples_v0/serde_attrs.rs b/docs/_includes/examples_v0/serde_attrs.rs new file mode 100644 index 00000000..b8d0fb41 --- /dev/null +++ b/docs/_includes/examples_v0/serde_attrs.rs @@ -0,0 +1,24 @@ +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 }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/serde_attrs.schema.json b/docs/_includes/examples_v0/serde_attrs.schema.json new file mode 100644 index 00000000..d0441932 --- /dev/null +++ b/docs/_includes/examples_v0/serde_attrs.schema.json @@ -0,0 +1,54 @@ +{ + "$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" + } + } + } + } + ] + } + } +} diff --git a/docs/_includes/examples_v0/validate.rs b/docs/_includes/examples_v0/validate.rs new file mode 100644 index 00000000..70fa4a2b --- /dev/null +++ b/docs/_includes/examples_v0/validate.rs @@ -0,0 +1,24 @@ +use schemars::{schema_for, JsonSchema}; + +#[derive(JsonSchema)] +pub struct MyStruct { + #[validate(range(min = 1, max = 10))] + pub my_int: i32, + pub my_bool: bool, + #[validate(required)] + pub my_nullable_enum: Option, +} + +#[derive(JsonSchema)] +pub enum MyEnum { + StringNewType(#[validate(email)] String), + StructVariant { + #[validate(length(min = 1, max = 100))] + floats: Vec, + }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples_v0/validate.schema.json b/docs/_includes/examples_v0/validate.schema.json new file mode 100644 index 00000000..1e45a969 --- /dev/null +++ b/docs/_includes/examples_v0/validate.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "type": "object", + "required": [ + "my_bool", + "my_int", + "my_nullable_enum" + ], + "properties": { + "my_bool": { + "type": "boolean" + }, + "my_int": { + "type": "integer", + "format": "int32", + "maximum": 10.0, + "minimum": 1.0 + }, + "my_nullable_enum": { + "oneOf": [ + { + "type": "object", + "required": [ + "StringNewType" + ], + "properties": { + "StringNewType": { + "type": "string", + "format": "phone" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "StructVariant" + ], + "properties": { + "StructVariant": { + "type": "object", + "required": [ + "floats" + ], + "properties": { + "floats": { + "type": "array", + "items": { + "type": "number", + "format": "float" + }, + "maxItems": 100, + "minItems": 1 + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/docs/_layouts/v0.md b/docs/_layouts/v0.md new file mode 100644 index 00000000..eb527e0b --- /dev/null +++ b/docs/_layouts/v0.md @@ -0,0 +1,10 @@ +--- +layout: default +--- + +
+

This page is for the current stable release of Schemars (v0.8.x). +

To view this page for the v1 version of Schemars, which is still under development, click here. +

+ +{{ content }} diff --git a/docs/_layouts/v1.md b/docs/_layouts/v1.md new file mode 100644 index 00000000..72aba814 --- /dev/null +++ b/docs/_layouts/v1.md @@ -0,0 +1,10 @@ +--- +layout: default +--- + +
+

This page is for the current v1 alpha version of Schemars. This version is still under development, and further breaking changes may be introduced. +

To view this page for the current stable release of Schemars (v0.8.x), click here. +

+ +{{ content }} diff --git a/docs/_sass/color_schemes/default.scss b/docs/_sass/color_schemes/default.scss index e8f98149..198ba4c0 100644 --- a/docs/_sass/color_schemes/default.scss +++ b/docs/_sass/color_schemes/default.scss @@ -4,7 +4,7 @@ $body-text-color: $grey-dk-200; $nav-child-link-color: $link-color; -$content-width: 900px; +$content-width: 60rem; $media-queries: ( xs: 320px, diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 47323dd9..75a0d5fa 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -12,14 +12,14 @@ pre.highlight, figure.highlight { line-height: 1.2em; } code { - font-size: 14px; + font-size: 0.85em; } // Always expand nav menu items -.nav-list .nav-list-item > .nav-list { +.nav-list:not(.nav-category-list) .nav-list-item > .nav-list { display: block; } -.nav-list-expander { +.nav-list:not(.nav-category-list) .nav-list-expander { display: none; } diff --git a/docs/_v0/1-deriving.md b/docs/_v0/1-deriving.md new file mode 100644 index 00000000..03f91c6e --- /dev/null +++ b/docs/_v0/1-deriving.md @@ -0,0 +1,36 @@ +--- +title: Deriving JsonSchema +nav_order: 2 +has_children: true +has_toc: false +permalink: /v0/deriving/ +--- + +# Deriving JsonSchema + +The most important trait in Schemars is `JsonSchema`, and the most important function of that trait is `json_schema(...)` which returns a JSON schema describing the type. Implementing this manually on many types would be slow and error-prone, so Schemars includes a derive macro which can implement that trait for you. Any derived implementation of `JsonSchema` should create a schema that describes the JSON representation of the type if it were to be serialized by serde_json. + +Usually, all you need to do to use it is to add a `#[derive(JsonSchema)]` attribute to your type: + +```rust +use schemars::{JsonSchema, schema_for}; + +#[derive(JsonSchema, Debug)] +struct Point { + x: i32, + y: i32, +} + +fn main() { + let schema = schema_for!(Point); + + let serialized = serde_json::to_string(&schema).unwrap(); + println!("{}", serialized); +} +``` + + diff --git a/docs/_v0/1.1-attributes.md b/docs/_v0/1.1-attributes.md new file mode 100644 index 00000000..ea877bba --- /dev/null +++ b/docs/_v0/1.1-attributes.md @@ -0,0 +1,323 @@ +--- +title: Attributes +parent: Deriving JsonSchema +nav_order: 1 +permalink: /v0/deriving/attributes/ +--- + + + +# Attributes + +You can add attributes to your types to customize Schemars's derived `JsonSchema` implementation. + +[Serde](https://serde.rs/) allows setting `#[serde(...)]` attributes which change how types are serialized, and Schemars will generally respect these attributes to ensure that generated schemas will match how the type is serialized by serde_json. `#[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. + +[Validator](https://github.com/Keats/validator) allows setting `#[validate(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes. + +
+ +TABLE OF CONTENTS + + +1. [Supported Serde Attributes](#supported-serde-attributes) + - [`rename`](#rename) + - [`rename_all`](#rename_all) + - [`tag` / `content` / `untagged`](#tag) + - [`default`](#default) + - [`skip`](#skip) + - [`skip_serializing`](#skip_serializing) + - [`skip_deserializing`](#skip_deserializing) + - [`flatten`](#flatten) + - [`with`](#with) + - [`bound`](#bound) +1. [Supported Validator Attributes](#supported-validator-attributes) + - [`email` / `phone` / `url`](#email-phone-url) + - [`length`](#length) + - [`range`](#range) + - [`regex`](#regex) + - [`contains`](#contains) + - [`required` / `required_nested`](#required) +1. [Other Attributes](#other-attributes) + - [`schema_with`](#schema_with) + - [`title` / `description`](#title-description) + - [`example`](#example) + - [`deprecated`](#deprecated) + - [`crate`](#crate) + - [Doc Comments (`doc`)](#doc) + +
+ +## Supported Serde Attributes + +
+ +

+ +`#[serde(rename = "name")]` / `#[schemars(rename = "name")]` + +

+ +Set on a struct, enum, field or variant to use the given name in the generated schema instead of the Rust name. When used on a struct or enum, the given name will be used as the title for root schemas, and the key within the root's `definitions` property for subschemas. + +If set on a struct or enum with generic type parameters, then the given name may contain them enclosed in curly braces (e.g. `{T}`) and they will be replaced with the concrete type names when the schema is generated. + +Serde docs: [container](https://serde.rs/container-attrs.html#rename) / [variant](https://serde.rs/variant-attrs.html#rename) / [field](https://serde.rs/field-attrs.html#rename) + +

+ +`#[serde(rename_all = "...")]` / `#[schemars(rename_all = "...")]` + +

+ +Set on a struct, enum or variant to rename all fields according to the given case convention (see the Serde docs for details). + +Serde docs: [container](https://serde.rs/container-attrs.html#rename_all) / [variant](https://serde.rs/variant-attrs.html#rename_all) + +

+ +`#[serde(tag = "type")]` / `#[schemars(tag = "type")]`
+`#[serde(tag = "t", content = "c")]` / `#[schemars(tag = "t", content = "c")]`
+`#[serde(untagged)]` / `#[schemars(untagged)]` + +

+ +Set on an enum to generate the schema for the [internally tagged](https://serde.rs/enum-representations.html#internally-tagged), [adjacently tagged](https://serde.rs/enum-representations.html#adjacently-tagged), or [untagged](https://serde.rs/enum-representations.html#untagged) representation of this enum. + +Serde docs: [`tag`](https://serde.rs/container-attrs.html#tag) / [`tag`+`content`](https://serde.rs/container-attrs.html#tag--content) / [`untagged`](https://serde.rs/container-attrs.html#untagged) + +

+ +`#[serde(default)]` / `#[schemars(default)]` / `#[serde(default = "path")]` / `#[schemars(default = "path")]` + +

+ +Set on a struct or field to give fields a default value, which excludes them from the schema's `required` properties. The default will also be set on the field's schema's `default` property, unless it is skipped by a [`skip_serializing_if`](https://serde.rs/field-attrs.html#skip_serializing_if) attribute on the field. Any [`serialize_with`](https://serde.rs/field-attrs.html#serialize_with) or [`with`](https://serde.rs/field-attrs.html#with) attribute set on the field will be used to serialize the default value. + +Serde docs: [container](https://serde.rs/container-attrs.html#default) / [field](https://serde.rs/field-attrs.html#default) + +

+ +`#[serde(skip)]` / `#[schemars(skip)]` + +

+ +Set on a variant or field to prevent it from appearing in any generated schema. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#skip) / [field](https://serde.rs/field-attrs.html#skip) + +

+ +`#[serde(skip_serializing)]` / `#[schemars(skip_serializing)]` + +

+ +Set on a field of a (non-tuple) struct to set the `writeOnly` property on that field's schema. Serde also allows this attribute on variants or tuple struct fields, but this will have no effect on generated schemas. + +Serde docs: [field](https://serde.rs/field-attrs.html#skip_deserializing) + +

+ +`#[serde(skip_deserializing)]` / `#[schemars(skip_deserializing)]` + +

+ +Set on a variant or field. When set on a field of a (non-tuple) struct, that field's schema will have the `readOnly` property set. When set on a variant or tuple struct field Schemars will treat this the same as a [`skip`](#skip) attribute. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#skip_deserializing) / [field](https://serde.rs/field-attrs.html#skip_deserializing) + +

+ +`#[serde(flatten)]` / `#[schemars(flatten)]` + +

+ +Set on a field to include that field's contents as though they belonged to the field's container. + +Serde docs: [field](https://serde.rs/field-attrs.html#flatten) + +

+ +`#[serde(with = "Type")]` / `#[schemars(with = "Type")]` + +

+ +Set on a variant or field to generate its schema as the given type instead of its actual type. Serde allows the `with` attribute to refer to any module path, but Schemars requires this to be an actual type which implements `JsonSchema`. + +If the given type has any required generic type parameters, then they must all be explicitly specified in this attribute. Serde frequently allows you to omit them as it can make use of type inference, but unfortunately this is not possible with Schemars. For example, `with = "Vec::"` will work, but `with = "Vec"` and `with = "Vec::<_>"` will not. + +Serde docs: [variant](https://serde.rs/variant-attrs.html#with) / [field](https://serde.rs/field-attrs.html#with) + +

+ +`#[serde(deny_unknown_fields)]` / `#[schemars(deny_unknown_fields)]` + +

+ +Setting this on a container will set the `additionalProperties` keyword on generated schemas to `false` to show that any extra properties are explicitly disallowed. + +Serde docs: [container](https://serde.rs/container-attrs.html#deny_unknown_fields) + +

+ +`#[serde(transparent)]` / `#[schemars(transparent)]` + +

+ +Set on a newtype struct or a braced struct with one field to make the struct's generated schema exactly the same as that of the single field's. + +Serde docs: [container](https://serde.rs/container-attrs.html#transparent) + +

+ +`#[schemars(bound = "...")]` + +

+ +Where-clause for the JsonSchema impl. This replaces any trait bounds inferred by schemars. Schemars does **not** use trait bounds from `#[serde(bound)]` attributes. + +Serde docs: [container](https://serde.rs/container-attrs.html#bound) + +
+ +## Supported Validator Attributes + +
+ +

+ +`#[validate(email)]` / `#[schemars(email)]`
+`#[validate(email)]` / `#[schemars(email)]`
+`#[validate(url)]` / `#[schemars(url)]` + +

+ +Sets the schema's `format` to `email`/`phone`/`uri`, as appropriate. Only one of these attributes may be present on a single field. + +Validator docs: [email](https://github.com/Keats/validator#email) / [phone](https://github.com/Keats/validator#phone) / [url](https://github.com/Keats/validator#url) + +

+ +`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`
+`#[validate(length(equal = 10))]` / `#[schemars(length(equal = 10))]` + +

+ +Sets the `minLength`/`maxLength` properties for string schemas, or the `minItems`/`maxItems` properties for array schemas. + +Validator docs: [length](https://github.com/Keats/validator#length) + +

+ +`#[validate(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]` + +

+ +Sets the `minimum`/`maximum` properties for number schemas. + +Validator docs: [range](https://github.com/Keats/validator#range) + +

+ +`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`
+`#[schemars(regex(pattern = r"^\d+$"))]` + +

+ +Sets the `pattern` property for string schemas. The `path::to::regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method. + +Providing an inline regex pattern using `regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. + +Validator docs: [regex](https://github.com/Keats/validator#regex) + +

+ +`#[validate(contains = "string")]` / `#[schemars(contains = "string")]` + +

+ +For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped. For object schemas (e.g. when the attribute is set on a HashMap field), includes the value in the `required` property, indicating that the map must contain it as a key. + +Validator docs: [contains](https://github.com/Keats/validator#contains) + +

+ +`#[validate(required)]` / `#[schemars(required)]`
+`#[validate(required_nested)]` + +

+ +When set on an `Option` field, this will create a schemas as though the field were a `T`. + +Validator docs: [required](https://github.com/Keats/validator#required) / [required_nested](https://github.com/Keats/validator#required_nested) + +
+ +## Other Attributes + +

+ +`#[schemars(schema_with = "some::function")]` + +

+ +Set on a variant or field to generate this field's schema using the given function. This function must be callable as `fn(&mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema`. + +

+ +`#[schemars(title = "Some title", description = "Some description")]` + +

+ +Set on a container, variant or field to set the generated schema's `title` and/or `description`. If present, these will be used instead of values from any [`doc` comments/attributes](#doc). + +

+ +`#[schemars(example = "some::function")]` + +

+ +Set on a container, variant or field to include the result of the given function in the generated schema's `examples`. The function should take no parameters and can return any type that implements serde's `Serialize` trait - it does not need to return the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. + +

+ +`#[deprecated]` + +

+ +Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rust-2018/the-compiler/an-attribute-for-deprecation.html) attribute on a struct, enum, field or variant to set the generated schema's `deprecated` keyword to `true`. + +

+ +`#[schemars(crate = "other_crate::schemars")]` + +

+ +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 = "..."]`) + +

+ +If a struct, variant or field has any [doc comments](https://doc.rust-lang.org/stable/rust-by-example/meta/doc.html#doc-comments) (or [`doc` attributes](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html)), then these will be used as the generated schema's `description`. If the first line is an ATX-style markdown heading (i.e. it begins with a # character), then it will be used as the schema's `title`, and the remaining lines will be the `description`. diff --git a/docs/_v0/2-implementing.md b/docs/_v0/2-implementing.md new file mode 100644 index 00000000..32f4edda --- /dev/null +++ b/docs/_v0/2-implementing.md @@ -0,0 +1,78 @@ +--- +title: Implementing JsonSchema +nav_order: 3 +permalink: /v0/implementing/ +--- + +# Implementing JsonSchema + +[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 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: + +```rust +fn schema_id() -> Cow<'static, str> { + Cow::Owned( + format!("[{}]", T::schema_id())) +} +``` + +`&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; +``` + +This function creates the JSON schema itself. The `gen` argument can be used to check the schema generation settings, or to get schemas for other types. If you do need schemas for other types, you should call the `gen.subschema_for::()` method instead of `::json_schema(gen)`, as `subschema_for` can add `T`'s schema to the root schema's `definitions` so that it does not need to be duplicated when used more than once. + +`json_schema` should not return a `$ref` schema. + +## is_referenceable (optional) + +```rust +fn is_referenceable() -> bool; +``` + +If this function returns `true`, then Schemars can re-use the generate schema where possible by adding it to the root schema's `definitions` and having other schemas reference it using the `$ref` keyword. This can greatly simplify schemas that include a particular type multiple times, especially if that type's schema is fairly complex. + +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. diff --git a/docs/_v0/3-generating.md b/docs/_v0/3-generating.md new file mode 100644 index 00000000..ec563491 --- /dev/null +++ b/docs/_v0/3-generating.md @@ -0,0 +1,35 @@ +--- +title: Generating Schemas +nav_order: 4 +permalink: /v0/generating/ +--- + +# Generating Schemas + +The easiest way to generate a schema for a type that implements is to use the [`schema_for!` macro](https://docs.rs/schemars/latest/schemars/macro.schema_for.html), like so: + +```rust +let my_schema = schema_for!(MyStruct); +``` + +This will create a schema that conforms 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 want more control over how the schema is generated, you can use the [`gen` module](https://docs.rs/schemars/latest/schemars/gen/). There are two main types in this module: + +- [`SchemaSettings`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaSettings.html), which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). +- [`SchemaGenerator`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaGenerator.html), which manages the generation of a schema document. + +See the API documentation for more info on how to use those types for custom schema generation. + +## 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 using the [`schema_for_value!` macro](https://docs.rs/schemars/latest/schemars/macro.schema_for_value.html). 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 +let value = MyStruct { foo = 123 }; +let my_schema = schema_for_value!(value); +``` + + diff --git a/docs/_v0/4-features.md b/docs/_v0/4-features.md new file mode 100644 index 00000000..4932fbed --- /dev/null +++ b/docs/_v0/4-features.md @@ -0,0 +1,39 @@ +--- +title: Feature Flags +nav_order: 5 +permalink: /v0/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) +- `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) +- `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: + +```toml +[dependencies] +schemars = { version = "0.8", features = ["chrono"] } +``` diff --git a/docs/_v0/5-examples.md b/docs/_v0/5-examples.md new file mode 100644 index 00000000..0df7e3df --- /dev/null +++ b/docs/_v0/5-examples.md @@ -0,0 +1,8 @@ +--- +title: Examples +nav_order: 6 +has_children: true +permalink: /v0/examples/ +--- + +# Examples diff --git a/docs/_v0/examples/1-derive_jsonschema.md b/docs/_v0/examples/1-derive_jsonschema.md new file mode 100644 index 00000000..590b7990 --- /dev/null +++ b/docs/_v0/examples/1-derive_jsonschema.md @@ -0,0 +1,12 @@ +--- +title: Deriving JsonSchema +parent: Examples +nav_order: 1 +summary: Deriving JsonSchema on a struct and enum. +--- + +# Deriving JsonSchema + +This is the simplest usage of Schemars. Both types are made to derive `JsonSchema`, and the `schema_for!` macro is used to generate the schema itself. + +{% include example_v0.md name="main" %} diff --git a/docs/_v0/examples/2-serde_attrs.md b/docs/_v0/examples/2-serde_attrs.md new file mode 100644 index 00000000..b6653194 --- /dev/null +++ b/docs/_v0/examples/2-serde_attrs.md @@ -0,0 +1,14 @@ +--- +title: Using Serde Attributes +parent: Examples +nav_order: 2 +summary: "Deriving JsonSchema on types that use #[serde] attributes to customise serialization behaviour." +--- + +# Using Serde Attributes + +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. + +The list of supported `#[serde]` attributes are [documented here]({{ site.baseurl }}{% link 1.1-attributes.md %}#supported-serde-attributes). + +{% include example_v0.md name="serde_attrs" %} diff --git a/docs/_v0/examples/3-schemars_attrs.md b/docs/_v0/examples/3-schemars_attrs.md new file mode 100644 index 00000000..2a5de96f --- /dev/null +++ b/docs/_v0/examples/3-schemars_attrs.md @@ -0,0 +1,12 @@ +--- +title: Using Schemars Attributes +parent: Examples +nav_order: 3 +summary: "Deriving JsonSchema on types that use #[schemars] attributes to customise serialization behaviour." +--- + +# Using Serde Attributes + +`#[serde(...)]` attributes can be overriden (or replaced) with `#[schemars(...)]` attributes, which behave identically. 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. + +{% include example_v0.md name="schemars_attrs" %} diff --git a/docs/_v0/examples/4-custom_settings.md b/docs/_v0/examples/4-custom_settings.md new file mode 100644 index 00000000..e439f501 --- /dev/null +++ b/docs/_v0/examples/4-custom_settings.md @@ -0,0 +1,12 @@ +--- +title: Custom Schema Settings +parent: Examples +nav_order: 4 +summary: Generating a schema using custom settings which changes how Option is handled. +--- + +# Custom Schema Settings + +The `gen` module allows you to customise how schemas are generated. For example, the default behaviour for `Option` is to include `null` in the schema's `type`s, but we can instead add a `nullable` property to its schema: + +{% include example_v0.md name="custom_settings" %} diff --git a/docs/_v0/examples/5-remote_derive.md b/docs/_v0/examples/5-remote_derive.md new file mode 100644 index 00000000..ee0adb69 --- /dev/null +++ b/docs/_v0/examples/5-remote_derive.md @@ -0,0 +1,16 @@ +--- +title: Derive for Remote Crate +parent: Examples +nav_order: 5 +summary: Deriving JsonSchema implementations for a type in somebody else's crate. +--- + +# Deriving JsonSchema for a Type in a Different Crate + +Rust's [orphan rule](https://doc.rust-lang.org/book/traits.html#rules-for-implementing-traits) requires that either the trait or the type for which you are implementing the trait must be defined in the same crate as the impl, so it is not possible to implement `JsonSchema` for a type in a different crate directly. + +To work around this, Schemars provides a way of deriving `JsonSchema` implementations for types in other people's crates. The only catch is that you have to provide a definition of the type for Schemars's derive to process. + +This is the same way that Serde allows remote deriving, which is why this page reads so similarly to [Serde's documentation](https://serde.rs/remote-derive.html)! + +{% include example_v0.md name="remote_derive" %} diff --git a/docs/_v0/examples/6-doc_comments.md b/docs/_v0/examples/6-doc_comments.md new file mode 100644 index 00000000..24de317b --- /dev/null +++ b/docs/_v0/examples/6-doc_comments.md @@ -0,0 +1,12 @@ +--- +title: Doc Comments +parent: Examples +nav_order: 6 +summary: Giving schemas a custom title and/or description using doc comments. +--- + +# Setting a Custom Title and/or Description Using Doc Comments + +If a struct, variant or field has any [doc comments](https://doc.rust-lang.org/stable/rust-by-example/meta/doc.html#doc-comments) (or [`doc` attributes](https://doc.rust-lang.org/rustdoc/the-doc-attribute.html)), then these will be used as the generated schema's `description`. If the first line is an ATX-style markdown heading (i.e. it begins with a # character), then it will be used as the schema's `title`, and the remaining lines will be the `description`. + +{% include example_v0.md name="doc_comments" %} diff --git a/docs/_v0/examples/7-custom_serialization.md b/docs/_v0/examples/7-custom_serialization.md new file mode 100644 index 00000000..499e6701 --- /dev/null +++ b/docs/_v0/examples/7-custom_serialization.md @@ -0,0 +1,19 @@ +--- +title: Custom Serialization +parent: Examples +nav_order: 7 +summary: >- + If a field has a #[serde(with = "path")] attribute where "path" is not a type that implements JsonSchema, + then in order to derive JsonSchema on the type, it must also have a #[schemars(with = "Type")] attribute, + where "Type" implements JsonSchema. +--- + +# Deriving JsonSchema with Fields Using Custom Serialization + +Serde allows you to change how a field is (de)serialized by setting a [`#[serde(with = "path")]`](https://serde.rs/field-attrs.html#with) attribute, where `$path::serialize` and `$path::deserialize` must be functions with the correct signature. Schemars supports the same attribute, but `path` must be a type implementing `JsonSchema`. + +In order to derive `JsonSchema` on a type which includes a `#[serde(with = "path")]` attribute where `path` is not a type implementing `JsonSchema`, you'll need to override it with a suitable `#[schemars(with = "Type")]` or `#[schemars(schema_with = "path")]` attribute. + +{% include example_v0.md name="custom_serialization" %} + +Note that the `default` values in the schema are serialized as strings where appropriate. diff --git a/docs/_v0/examples/8-enum_repr.md b/docs/_v0/examples/8-enum_repr.md new file mode 100644 index 00000000..1cfcf819 --- /dev/null +++ b/docs/_v0/examples/8-enum_repr.md @@ -0,0 +1,13 @@ +--- +title: Serialize Enum as Number (serde_repr) +parent: Examples +nav_order: 8 +summary: >- + Generating a schema for with a C-like enum compatible with serde_repr. +--- + +# Serialize Enum as Number (serde_repr Compatibility) + +If you use the `#[repr(...)]` attribute on an enum to give it a C-like representation, then you may also want to use the [serde_repr](https://github.com/dtolnay/serde-repr) crate to serialize the enum values as numbers. In this case, you should use the corresponding `JsonSchema_repr` derive to ensure the schema for your type reflects how serde formats your type. + +{% include example_v0.md name="enum_repr" %} diff --git a/docs/_v0/examples/9-from_value.md b/docs/_v0/examples/9-from_value.md new file mode 100644 index 00000000..45198ff2 --- /dev/null +++ b/docs/_v0/examples/9-from_value.md @@ -0,0 +1,15 @@ +--- +title: Generate Schema from Example Value +parent: Examples +nav_order: 9 +summary: >- + Generating a schema for a serializable value. +--- + +# Generate Schema from Example Value + +If you want a schema for a type that can't/doesn't implement `JsonSchema`, but does implement [`serde::Serialize`](https://docs.serde.rs/serde/trait.Serialize.html), 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. + +{% include example_v0.md name="from_value" %} + +Note that the schema for the enum is not very useful in this case, since schemars doesn't know anything about the second variant. diff --git a/docs/_v0/index.md b/docs/_v0/index.md new file mode 100644 index 00000000..3b10e0d2 --- /dev/null +++ b/docs/_v0/index.md @@ -0,0 +1,20 @@ +--- +title: Overview +has_children: true +nav_order: 1 +permalink: /v0/ +--- + +# Schemars + +Schemars is a library to generate JSON Schema documents from Rust data structures. + +This is built on Rust's trait system - any type which implements the [`JsonSchema`](https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html) trait can have a JSON Schema generated describing that type. Schemars implements this on many standard library types, and provides a derive macro to automatically implement it on custom types. + +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. + +## 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. + +{% include example.md name="main" %} diff --git a/docs/assets/js/search-data.json b/docs/assets/js/search-data.json deleted file mode 100644 index 49cbc9dc..00000000 --- a/docs/assets/js/search-data.json +++ /dev/null @@ -1,12 +0,0 @@ ---- ---- -{ - {% for page in site.html_pages %}{% if page.search_exclude != true %}"{{ forloop.index0 }}": { - "id": "{{ forloop.index0 }}", - "title": "{{ page.title | replace: '&', '&' }}", - "content": "{{ page.content | markdownify | strip_html | escape_once | remove: 'Table of contents' | remove: '```' | remove: '---' | replace: '\', ' ' | normalize_whitespace }}", - "url": "{{ page.url | absolute_url }}", - "relUrl": "{{ page.url }}" - }{% unless forloop.last %},{% endunless %} - {% endif %}{% endfor %} -} diff --git a/docs/assets/js/zzzz-search-data.json b/docs/assets/js/zzzz-search-data.json new file mode 100644 index 00000000..a04b7916 --- /dev/null +++ b/docs/assets/js/zzzz-search-data.json @@ -0,0 +1,75 @@ +--- +layout: null +permalink: /assets/js/search-data.json +--- +{ +{%- assign i = 0 -%} +{%- assign pages_array = "" | split: "" -%} +{%- assign pages_array = pages_array | push: site.html_pages -%} +{%- if site.just_the_docs.collections -%} + {%- for collection_entry in site.just_the_docs.collections -%} + {%- assign collection_key = collection_entry[0] -%} + {%- assign collection_value = collection_entry[1] -%} + {%- assign collection = site[collection_key] -%} + {%- if collection_value.search_exclude != true -%} + {%- assign pages_array = pages_array | push: collection -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} +{%- for pages in pages_array -%} + {%- for page in pages -%} + {%- if page.title and page.search_exclude != true -%} + {%- assign page_content = page.content -%} + {%- assign heading_level = site.search.heading_level | default: 2 -%} + {%- for j in (2..heading_level) -%} + {%- assign tag = '' -%} + {%- assign title = titleAndContent[0] | replace_first: '>', '

' | split: '

' -%} + {%- assign title = title[1] | strip_html -%} + {%- assign content = titleAndContent[1] -%} + {%- assign url = page.url -%} + {%- if title == page.title and parts[0] == '' -%} + {%- assign title_found = true -%} + {%- else -%} + {%- assign id = titleAndContent[0] -%} + {%- assign id = id | split: 'id="' -%} + {%- if id.size == 2 -%} + {%- assign id = id[1] -%} + {%- assign id = id | split: '"' -%} + {%- assign id = id[0] -%} + {%- capture url -%}{{ url | append: '#' | append: id }}{%- endcapture -%} + {%- endif -%} + {%- endif -%} + {%- unless i == 0 -%},{%- endunless -%} + "{{ i }}": { + "doc": {{ page.title | jsonify }}, + "title": {{ title | jsonify }}, + "content": {{ content | replace: ' i # Custom Schema Settings -The `gen` module allows you to customise how schemas are generated. For example, the default behaviour for `Option` is to include `null` in the schema's `type`s, but we can instead add a `nullable` property to its schema: +The `generate` module allows you to customise how schemas are generated. For example, the default behaviour for `Option` is to include `null` in the schema's `type`s, but we can instead add a `nullable` property to its schema: {% include example.md name="custom_settings" %} diff --git a/docs/examples/5-remote_derive.md b/docs/examples/5-remote_derive.md index 93e99c13..fd7f74ff 100644 --- a/docs/examples/5-remote_derive.md +++ b/docs/examples/5-remote_derive.md @@ -1,5 +1,4 @@ --- -layout: default title: Derive for Remote Crate parent: Examples nav_order: 5 diff --git a/docs/examples/6-doc_comments.md b/docs/examples/6-doc_comments.md index 9a5cdf9c..66f4a491 100644 --- a/docs/examples/6-doc_comments.md +++ b/docs/examples/6-doc_comments.md @@ -1,5 +1,4 @@ --- -layout: default title: Doc Comments parent: Examples nav_order: 6 diff --git a/docs/examples/7-custom_serialization.md b/docs/examples/7-custom_serialization.md index 8caa930f..b124ae77 100644 --- a/docs/examples/7-custom_serialization.md +++ b/docs/examples/7-custom_serialization.md @@ -1,5 +1,4 @@ --- -layout: default title: Custom Serialization parent: Examples nav_order: 7 @@ -13,7 +12,7 @@ summary: >- Serde allows you to change how a field is (de)serialized by setting a [`#[serde(with = "path")]`](https://serde.rs/field-attrs.html#with) attribute, where `$path::serialize` and `$path::deserialize` must be functions with the correct signature. Schemars supports the same attribute, but `path` must be a type implementing `JsonSchema`. -In order to derive `JsonSchema` on a type which includes a `#[serde(with = "path")]` attribute where `path` is not a type implementing `JsonSchema`, you'll need to override it with a suitable `#[schemars(with = "Type")]` or `#[schemars(schema_with = "path")]` attribute. +In order to derive `JsonSchema` on a type which includes a `#[serde(with = "path")]` attribute where `path` is not a type implementing `JsonSchema`, you'll need to override it with a suitable `#[schemars(with = "Type")]` or `#[schemars(schema_with = "path")]` attribute. {% include example.md name="custom_serialization" %} diff --git a/docs/examples/8-enum_repr.md b/docs/examples/8-enum_repr.md index 0312c29b..9533cbaf 100644 --- a/docs/examples/8-enum_repr.md +++ b/docs/examples/8-enum_repr.md @@ -1,5 +1,4 @@ --- -layout: default title: Serialize Enum as Number (serde_repr) parent: Examples nav_order: 8 diff --git a/docs/examples/9-from_value.md b/docs/examples/9-from_value.md index b711be0d..89229255 100644 --- a/docs/examples/9-from_value.md +++ b/docs/examples/9-from_value.md @@ -1,5 +1,4 @@ --- -layout: default title: Generate Schema from Example Value parent: Examples nav_order: 9 diff --git a/docs/index.md b/docs/index.md index 28683429..0601c216 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,4 @@ --- -layout: default title: Overview nav_order: 1 --- @@ -8,9 +7,9 @@ nav_order: 1 Schemars is a library to generate JSON Schema documents from Rust data structures. -This is built on Rust's trait system - any type which implements the [`JsonSchema`](https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html) trait can have a JSON Schema generated describing that type. Schemars implements this on many standard library types, and provides a derive macro to automatically implement it on custom types. +This is built on Rust's trait system - any type which implements the [`JsonSchema`](https://docs.rs/schemars/1.0.0--latest/schemars/trait.JsonSchema.html) trait can have a JSON Schema generated describing that type. Schemars implements this on many standard library types, and provides a derive macro to automatically implement it on custom types. -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. ## Basic Usage diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index 34573c65..d519d6df 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -3,110 +3,124 @@ 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 = "1.0.0-alpha.14" authors = ["Graham Esau "] -edition = "2018" +edition = "2021" license = "MIT" readme = "README.md" keywords = ["rust", "json-schema", "serde"] -categories = ["encoding"] -build = "build.rs" +categories = ["encoding", "no-std"] +rust-version = "1.65" [dependencies] -schemars_derive = { version = "=0.8.12", optional = true, path = "../schemars_derive" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.25" +schemars_derive = { version = "=1.0.0-alpha.14", optional = true, path = "../schemars_derive" } +serde = { version = "1.0", default-features = false, features = ["alloc"] } +serde_json = { version = "1.0.127", default-features = false, features = ["alloc"] } dyn-clone = "1.0" +ref-cast = "1.0.22" -chrono = { version = "0.4", default-features = false, optional = true } -indexmap = { version = "1.2", features = ["serde-1"], optional = true } -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" } -smallvec = { version = "1.0", optional = true } -arrayvec05 = { version = "0.5", default-features = false, optional = true, package = "arrayvec" } +# optional dependencies arrayvec07 = { version = "0.7", default-features = false, optional = true, package = "arrayvec" } -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 } -enumset = { version = "1.0", optional = true } -smol_str = { version = "0.1.17", optional = true } +bigdecimal04 = { version = "0.4", default-features = false, optional = true, package = "bigdecimal" } +bytes1 = { version = "1.0", default-features = false, optional = true, package = "bytes" } +chrono04 = { version = "0.4", default-features = false, optional = true, package = "chrono" } +either1 = { version = "1.3", default-features = false, optional = true, package = "either" } +enumset1 = { version = "1.0", default-features = false, optional = true, package = "enumset" } +indexmap2 = { version = "2.0", default-features = false, optional = true, package = "indexmap" } +rust_decimal1 = { version = "1", default-features = false, optional = true, package = "rust_decimal" } +semver1 = { version = "1.0.9", default-features = false, optional = true, package = "semver" } +smallvec1 = { version = "1.0", default-features = false, optional = true, package = "smallvec" } +smol_str02 = { version = "0.2.1", default-features = false, optional = true, package = "smol_str" } +url2 = { version = "2.0", default-features = false, optional = true, package = "url" } +uuid1 = { version = "1.0", default-features = false, optional = true, package = "uuid" } triomphe = { version = "0.1.8", optional = true } [dev-dependencies] pretty_assertions = "1.2.1" trybuild = "1.0" +serde = { version = "1.0", features = ["derive"] } [features] -default = ["derive"] +default = ["derive", "std"] + +# Provide impls for common standard library types like `HashMap`. +# Requires a dependency on the Rust standard library. +std = [] +# Provide `derive(JsonSchema)` macro. derive = ["schemars_derive"] -# Use a different representation for the map type of Schemars. -# This allows data to be read into a Value and written back to a JSON string -# while preserving the order of map keys in the input. -preserve_order = ["indexmap"] +# Preserves order of properties inserted into a `Schema`. +# When deriving `JsonSchema`, this ensures that the `properties` entires match +# the order of the fields in the struct definition. +preserve_order = ["serde_json/preserve_order"] -impl_json_schema = ["derive"] -# derive_json_schema will be removed in a later version -derive_json_schema = ["impl_json_schema"] +# Implements `JsonSchema` on `serde_json::value::RawValue` +raw_value = ["serde_json/raw_value"] -# `uuid` feature contains `uuid08` only for back-compat - will be changed to include uuid 1.0 instead in a later version -uuid = ["uuid08"] -# `arrayvec` feature without version suffix is included only for back-compat - will be removed in a later version -arrayvec = ["arrayvec05"] -indexmap1 = ["indexmap"] +# For internal/CI use only +_ui_test = [] + +[[test]] +name = "std_time" +required-features = ["std"] + +[[test]] +name = "ffi" +required-features = ["std"] -ui_test = [] +[[test]] +name = "ui" +required-features = ["_ui_test"] [[test]] name = "chrono" -required-features = ["chrono"] +required-features = ["chrono04"] [[test]] name = "indexmap" -required-features = ["indexmap"] +required-features = ["indexmap2"] [[test]] name = "either" -required-features = ["either"] +required-features = ["either1"] [[test]] name = "uuid" -required-features = ["uuid08", "uuid1"] +required-features = ["uuid1"] [[test]] name = "smallvec" -required-features = ["smallvec"] +required-features = ["smallvec1"] [[test]] name = "bytes" -required-features = ["bytes"] +required-features = ["bytes1"] [[test]] name = "arrayvec" -required-features = ["arrayvec05", "arrayvec07"] +required-features = ["arrayvec07"] [[test]] -name = "schema_for_schema" -required-features = ["impl_json_schema"] +name = "url" +required-features = ["url2"] [[test]] -name = "ui" -required-features = ["ui_test"] +name = "enumset" +required-features = ["enumset1"] [[test]] -name = "url" -required-features = ["url"] +name = "smol_str" +required-features = ["smol_str02"] [[test]] -name = "enumset" -required-features = ["enumset"] +name = "semver" +required-features = ["semver1"] [[test]] -name = "smol_str" -required-features = ["smol_str"] +name = "decimal" +required-features = ["rust_decimal1", "bigdecimal04"] [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--extend-css", "docs-rs-custom.css"] diff --git a/schemars/build.rs b/schemars/build.rs deleted file mode 100644 index 6f704fe3..00000000 --- a/schemars/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::env; - -// Based on https://github.com/serde-rs/serde/blob/master/serde/build.rs - -fn main() { - let target = env::var("TARGET").unwrap(); - let emscripten = target == "asmjs-unknown-emscripten" || target == "wasm32-unknown-emscripten"; - - // Whitelist of archs that support std::sync::atomic module. Ideally we - // would use #[cfg(target_has_atomic = "...")] but it is not stable yet. - // Instead this is based on rustc's src/librustc_target/spec/*.rs. - let has_atomic64 = target.starts_with("x86_64") - || target.starts_with("i686") - || target.starts_with("aarch64") - || target.starts_with("powerpc64") - || target.starts_with("sparc64") - || target.starts_with("mips64el"); - let has_atomic32 = has_atomic64 || emscripten; - if has_atomic64 { - println!("cargo:rustc-cfg=std_atomic64"); - } - if has_atomic32 { - println!("cargo:rustc-cfg=std_atomic"); - } -} diff --git a/schemars/docs-rs-custom.css b/schemars/docs-rs-custom.css new file mode 120000 index 00000000..594119fe --- /dev/null +++ b/schemars/docs-rs-custom.css @@ -0,0 +1 @@ +../docs-rs-custom.css \ No newline at end of file diff --git a/schemars/examples/custom_serialization.rs b/schemars/examples/custom_serialization.rs index 53c78fa6..8e2a695a 100644 --- a/schemars/examples/custom_serialization.rs +++ b/schemars/examples/custom_serialization.rs @@ -1,5 +1,4 @@ -use schemars::schema::{Schema, SchemaObject}; -use schemars::{gen::SchemaGenerator, schema_for, JsonSchema}; +use schemars::{schema_for, JsonSchema, Schema, SchemaGenerator}; use serde::{Deserialize, Serialize}; // `int_as_string` and `bool_as_string` use the schema for `String`. @@ -20,10 +19,12 @@ pub struct MyStruct { pub bool_normal: bool, } -fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = ::json_schema(gen).into(); - schema.format = Some("boolean".to_owned()); - schema.into() +fn make_custom_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = String::json_schema(generator); + schema + .ensure_object() + .insert("format".into(), "boolean".into()); + schema } fn eight() -> i32 { diff --git a/schemars/examples/custom_serialization.schema.json b/schemars/examples/custom_serialization.schema.json index 42fda993..1aa09420 100644 --- a/schemars/examples/custom_serialization.schema.json +++ b/schemars/examples/custom_serialization.schema.json @@ -1,25 +1,25 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", "properties": { "bool_as_string": { - "default": "false", "type": "string", - "format": "boolean" + "format": "boolean", + "default": "false" }, "bool_normal": { - "default": false, - "type": "boolean" + "type": "boolean", + "default": false }, "int_as_string": { - "default": "8", - "type": "string" + "type": "string", + "default": "8" }, "int_normal": { - "default": 8, "type": "integer", - "format": "int32" + "format": "int32", + "default": 8 } } } diff --git a/schemars/examples/custom_settings.rs b/schemars/examples/custom_settings.rs index 335daf3c..7858720f 100644 --- a/schemars/examples/custom_settings.rs +++ b/schemars/examples/custom_settings.rs @@ -1,4 +1,4 @@ -use schemars::{gen::SchemaSettings, JsonSchema}; +use schemars::{generate::SchemaSettings, JsonSchema}; #[derive(JsonSchema)] pub struct MyStruct { @@ -18,7 +18,7 @@ fn main() { s.option_nullable = true; s.option_add_null_type = false; }); - let gen = settings.into_generator(); - let schema = gen.into_root_schema_for::(); + let generator = settings.into_generator(); + let schema = generator.into_root_schema_for::(); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } diff --git a/schemars/examples/custom_settings.schema.json b/schemars/examples/custom_settings.schema.json index 12ac7d59..8da43482 100644 --- a/schemars/examples/custom_settings.schema.json +++ b/schemars/examples/custom_settings.schema.json @@ -2,10 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "type": "boolean" @@ -23,32 +19,30 @@ "nullable": true } }, + "required": [ + "my_int", + "my_bool" + ], "definitions": { "MyEnum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -57,10 +51,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/schemars/examples/doc_comments.schema.json b/schemars/examples/doc_comments.schema.json index 121cdb42..a3aa7670 100644 --- a/schemars/examples/doc_comments.schema.json +++ b/schemars/examples/doc_comments.schema.json @@ -1,12 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "My Amazing Struct", - "description": "This struct shows off generating a schema with a custom title and description.", + "description": "This struct shows off generating a schema with\n a custom title and description.", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "description": "This bool has a description, but no title.", @@ -22,7 +18,7 @@ "description": "This enum might be set, or it might not.", "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" @@ -30,35 +26,33 @@ ] } }, - "definitions": { + "required": [ + "my_int", + "my_bool" + ], + "$defs": { "MyEnum": { "title": "My Amazing Enum", "oneOf": [ { "description": "A wrapper around a `String`", "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { - "description": "A struct-like enum variant which contains some floats", + "description": "A struct-like enum variant which contains\n some floats", "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "description": "The floats themselves", @@ -68,10 +62,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/schemars/examples/enum_repr.schema.json b/schemars/examples/enum_repr.schema.json index 04841b7d..22d2233f 100644 --- a/schemars/examples/enum_repr.schema.json +++ b/schemars/examples/enum_repr.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "SmallPrime", "type": "integer", "enum": [ diff --git a/schemars/examples/from_value.schema.json b/schemars/examples/from_value.schema.json index 4ba77351..64bad01c 100644 --- a/schemars/examples/from_value.schema.json +++ b/schemars/examples/from_value.schema.json @@ -1,15 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", - "examples": [ - { - "my_bool": true, - "my_int": 123, - "my_nullable_enum": { - "StringNewType": "foo" - } - } - ], "type": "object", "properties": { "my_bool": { @@ -19,5 +10,14 @@ "type": "integer" }, "my_nullable_enum": true - } + }, + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ] } diff --git a/schemars/examples/main.schema.json b/schemars/examples/main.schema.json index ddbd9d33..5eab9093 100644 --- a/schemars/examples/main.schema.json +++ b/schemars/examples/main.schema.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int" - ], "properties": { "my_bool": { "type": "boolean" @@ -17,7 +13,7 @@ "my_nullable_enum": { "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" @@ -25,32 +21,30 @@ ] } }, - "definitions": { + "required": [ + "my_int", + "my_bool" + ], + "$defs": { "MyEnum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -59,10 +53,16 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } diff --git a/schemars/examples/remote_derive.schema.json b/schemars/examples/remote_derive.schema.json index e5841778..df697510 100644 --- a/schemars/examples/remote_derive.schema.json +++ b/schemars/examples/remote_derive.schema.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Process", "type": "object", - "required": [ - "command_line", - "durations", - "wall_time" - ], "properties": { "command_line": { "type": "string" @@ -14,20 +9,21 @@ "durations": { "type": "array", "items": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" } }, "wall_time": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" } }, - "definitions": { + "required": [ + "command_line", + "wall_time", + "durations" + ], + "$defs": { "Duration": { "type": "object", - "required": [ - "nanos", - "secs" - ], "properties": { "nanos": { "type": "integer", @@ -37,7 +33,11 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "secs", + "nanos" + ] } } } diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs index cd69b527..21c3bdcf 100644 --- a/schemars/examples/schemars_attrs.rs +++ b/schemars/examples/schemars_attrs.rs @@ -1,27 +1,33 @@ -use schemars::{schema_for, JsonSchema}; +use schemars::{schema_for, JsonSchema, Schema}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, JsonSchema)] -#[schemars(rename_all = "camelCase", deny_unknown_fields)] +#[schemars(rename_all = "camelCase", deny_unknown_fields, extend("x-customProperty" = "example"))] pub struct MyStruct { #[serde(rename = "thisIsOverridden")] - #[schemars(rename = "myNumber", range(min = 1, max = 10))] + #[schemars(rename = "myNumber", range(min = 1, max = 10), transform = remove_format)] pub my_int: i32, 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)] #[schemars(untagged)] pub enum MyEnum { - StringNewType(#[schemars(phone)] String), + StringNewType(#[schemars(email)] String), StructVariant { #[schemars(length(min = 1, max = 100))] floats: Vec, }, } +fn remove_format(schema: &mut Schema) { + schema.remove("format"); +} + fn main() { let schema = schema_for!(MyStruct); println!("{}", serde_json::to_string_pretty(&schema).unwrap()); diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json index 958cb6bb..19a613c2 100644 --- a/schemars/examples/schemars_attrs.schema.json +++ b/schemars/examples/schemars_attrs.schema.json @@ -1,46 +1,51 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { - "default": null, "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" } - ] + ], + "default": null }, "myNumber": { "type": "integer", - "format": "int32", - "maximum": 10.0, - "minimum": 1.0 + "maximum": 10, + "minimum": 1 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, - "definitions": { + "required": [ + "myNumber", + "myBool", + "myVecStr" + ], + "x-customProperty": "example", + "$defs": { "MyEnum": { "anyOf": [ { "type": "string", - "format": "phone" + "format": "email" }, { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -51,7 +56,10 @@ "maxItems": 100, "minItems": 1 } - } + }, + "required": [ + "floats" + ] } ] } diff --git a/schemars/examples/serde_attrs.schema.json b/schemars/examples/serde_attrs.schema.json index d0441932..81798994 100644 --- a/schemars/examples/serde_attrs.schema.json +++ b/schemars/examples/serde_attrs.schema.json @@ -1,25 +1,21 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "myBool", - "myNumber" - ], "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { - "default": null, "anyOf": [ { - "$ref": "#/definitions/MyEnum" + "$ref": "#/$defs/MyEnum" }, { "type": "null" } - ] + ], + "default": null }, "myNumber": { "type": "integer", @@ -27,7 +23,11 @@ } }, "additionalProperties": false, - "definitions": { + "required": [ + "myNumber", + "myBool" + ], + "$defs": { "MyEnum": { "anyOf": [ { @@ -35,9 +35,6 @@ }, { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -46,7 +43,10 @@ "format": "float" } } - } + }, + "required": [ + "floats" + ] } ] } diff --git a/schemars/examples/validate.rs b/schemars/examples/validate.rs index 41169765..70fa4a2b 100644 --- a/schemars/examples/validate.rs +++ b/schemars/examples/validate.rs @@ -11,7 +11,7 @@ pub struct MyStruct { #[derive(JsonSchema)] pub enum MyEnum { - StringNewType(#[validate(phone)] String), + StringNewType(#[validate(email)] String), StructVariant { #[validate(length(min = 1, max = 100))] floats: Vec, diff --git a/schemars/examples/validate.schema.json b/schemars/examples/validate.schema.json index 1e45a969..c7728b8e 100644 --- a/schemars/examples/validate.schema.json +++ b/schemars/examples/validate.schema.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "my_bool", - "my_int", - "my_nullable_enum" - ], "properties": { "my_bool": { "type": "boolean" @@ -14,35 +9,29 @@ "my_int": { "type": "integer", "format": "int32", - "maximum": 10.0, - "minimum": 1.0 + "maximum": 10, + "minimum": 1 }, "my_nullable_enum": { "oneOf": [ { "type": "object", - "required": [ - "StringNewType" - ], "properties": { "StringNewType": { "type": "string", - "format": "phone" + "format": "email" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StringNewType" + ] }, { "type": "object", - "required": [ - "StructVariant" - ], "properties": { "StructVariant": { "type": "object", - "required": [ - "floats" - ], "properties": { "floats": { "type": "array", @@ -53,12 +42,23 @@ "maxItems": 100, "minItems": 1 } - } + }, + "required": [ + "floats" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "StructVariant" + ] } ] } - } + }, + "required": [ + "my_int", + "my_bool", + "my_nullable_enum" + ] } diff --git a/schemars/src/_private.rs b/schemars/src/_private.rs deleted file mode 100644 index aa6b570a..00000000 --- a/schemars/src/_private.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::flatten::Merge; -use crate::gen::SchemaGenerator; -use crate::schema::{Metadata, Schema, SchemaObject}; -use crate::JsonSchema; -use serde::Serialize; -use serde_json::Value; - -// Helper for generating schemas for flattened `Option` fields. -pub fn json_schema_for_flatten( - gen: &mut SchemaGenerator, - required: bool, -) -> Schema { - let mut schema = T::_schemars_private_non_optional_json_schema(gen); - - if T::_schemars_private_is_option() && !required { - if let Schema::Object(SchemaObject { - object: Some(ref mut object_validation), - .. - }) = schema - { - object_validation.required.clear(); - } - } - - schema -} - -pub fn apply_metadata(schema: Schema, metadata: Metadata) -> Schema { - if metadata == Metadata::default() { - schema - } else { - let mut schema_obj = schema.into_object(); - schema_obj.metadata = Some(Box::new(metadata)).merge(schema_obj.metadata); - Schema::Object(schema_obj) - } -} - -/// Hack to simulate specialization: -/// `MaybeSerializeWrapper(x).maybe_to_value()` will resolve to either -/// - The inherent method `MaybeSerializeWrapper::maybe_to_value(...)` if x is `Serialize` -/// - The trait method `NoSerialize::maybe_to_value(...)` from the blanket impl otherwise -#[doc(hidden)] -#[macro_export] -macro_rules! _schemars_maybe_to_value { - ($expression:expr) => {{ - #[allow(unused_imports)] - use $crate::_private::{MaybeSerializeWrapper, NoSerialize as _}; - - MaybeSerializeWrapper($expression).maybe_to_value() - }}; -} - -pub struct MaybeSerializeWrapper(pub T); - -pub trait NoSerialize: Sized { - fn maybe_to_value(self) -> Option { - None - } -} - -impl NoSerialize for T {} - -impl MaybeSerializeWrapper { - pub fn maybe_to_value(self) -> Option { - serde_json::value::to_value(self.0).ok() - } -} diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs new file mode 100644 index 00000000..8fb532d1 --- /dev/null +++ b/schemars/src/_private/mod.rs @@ -0,0 +1,305 @@ +use crate::_alloc_prelude::*; +use crate::transform::transform_immediate_subschemas; +use crate::{JsonSchema, Schema, SchemaGenerator}; +use serde::Serialize; +use serde_json::{json, map::Entry, Map, Value}; + +mod regex_syntax; +mod rustdoc; + +pub extern crate alloc; +pub extern crate serde_json; + +pub use rustdoc::get_title_and_description; + +// Helper for generating schemas for flattened `Option` fields. +pub fn json_schema_for_flatten( + generator: &mut SchemaGenerator, + required: bool, +) -> Schema { + let mut schema = T::_schemars_private_non_optional_json_schema(generator); + + if T::_schemars_private_is_option() && !required { + schema.remove("required"); + } + + // Always allow aditional/unevaluated properties, because the outer struct determines + // whether it denies unknown fields. + allow_unknown_properties(&mut schema); + + schema +} + +fn allow_unknown_properties(schema: &mut Schema) { + if schema.get("additionalProperties").and_then(Value::as_bool) == Some(false) { + schema.remove("additionalProperties"); + } + if schema.get("unevaluatedProperties").and_then(Value::as_bool) == Some(false) { + schema.remove("unevaluatedProperties"); + } + + transform_immediate_subschemas(&mut allow_unknown_properties, schema); +} + +/// Hack to simulate specialization: +/// `MaybeSerializeWrapper(x).maybe_to_value()` will resolve to either +/// - The inherent method `MaybeSerializeWrapper::maybe_to_value(...)` if x is `Serialize` +/// - The trait method `NoSerialize::maybe_to_value(...)` from the blanket impl otherwise +#[doc(hidden)] +#[macro_export] +macro_rules! _schemars_maybe_to_value { + ($expression:expr) => {{ + #[allow(unused_imports)] + use $crate::_private::{MaybeSerializeWrapper, NoSerialize as _}; + + MaybeSerializeWrapper($expression).maybe_to_value() + }}; +} + +pub struct MaybeSerializeWrapper(pub T); + +pub trait NoSerialize: Sized { + fn maybe_to_value(self) -> Option { + None + } +} + +impl NoSerialize for T {} + +impl MaybeSerializeWrapper { + pub fn maybe_to_value(self) -> Option { + serde_json::value::to_value(self.0).ok() + } +} + +/// Create a schema for a unit enum variant +pub fn new_unit_enum_variant(variant: &str) -> Schema { + json_schema!({ + "type": "string", + "const": variant, + }) +} + +/// Create a schema for an externally tagged enum variant +#[allow(clippy::needless_pass_by_value)] +pub fn new_externally_tagged_enum_variant(variant: &str, sub_schema: Schema) -> Schema { + // TODO: this can be optimised by inserting the `sub_schema` as a `Value` rather than + // using the `json_schema!` macro which borrows and serializes the sub_schema + json_schema!({ + "type": "object", + "properties": { + variant: sub_schema + }, + "required": [variant], + "additionalProperties": false, + }) +} + +/// Update a schema for an internally tagged enum variant +pub fn apply_internal_enum_variant_tag( + schema: &mut Schema, + tag_name: &str, + variant: &str, + deny_unknown_fields: bool, +) { + let obj = schema.ensure_object(); + let is_unit = obj.get("type").and_then(|t| t.as_str()) == Some("null"); + + obj.insert("type".to_owned(), "object".into()); + + if let Some(properties) = obj + .entry("properties") + .or_insert(Value::Object(Map::new())) + .as_object_mut() + { + properties.insert( + tag_name.to_string(), + json!({ + "type": "string", + "const": variant + }), + ); + } + + if let Some(required) = obj + .entry("required") + .or_insert(Value::Array(Vec::new())) + .as_array_mut() + { + required.insert(0, tag_name.into()); + } + + if deny_unknown_fields && is_unit { + obj.entry("additionalProperties").or_insert(false.into()); + } +} + +pub fn insert_object_property( + schema: &mut Schema, + key: &str, + has_default: bool, + required: bool, + sub_schema: Schema, +) { + fn insert_object_property_impl( + schema: &mut Schema, + key: &str, + has_default: bool, + required: bool, + sub_schema: Schema, + ) { + let obj = schema.ensure_object(); + if let Some(properties) = obj + .entry("properties") + .or_insert(Value::Object(Map::new())) + .as_object_mut() + { + properties.insert(key.to_owned(), sub_schema.into()); + } + + if !has_default && (required) { + if let Some(req) = obj + .entry("required") + .or_insert(Value::Array(Vec::new())) + .as_array_mut() + { + req.push(key.into()); + } + } + } + + let required = required || !T::_schemars_private_is_option(); + insert_object_property_impl(schema, key, has_default, required, sub_schema); +} + +pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into) { + schema.ensure_object().insert(key.to_owned(), value.into()); +} + +pub fn insert_metadata_property_if_nonempty( + schema: &mut Schema, + key: &str, + value: impl Into, +) { + let value: String = value.into(); + if !value.is_empty() { + insert_metadata_property(schema, key, value); + } +} + +pub fn insert_validation_property( + schema: &mut Schema, + required_type: &str, + key: &str, + value: impl Into, +) { + if schema.has_type(required_type) || (required_type == "number" && schema.has_type("integer")) { + schema.ensure_object().insert(key.to_owned(), value.into()); + } +} + +pub fn must_contain(schema: &mut Schema, substring: &str) { + let escaped = regex_syntax::escape(substring); + insert_validation_property(schema, "string", "pattern", escaped); +} + +pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) { + if let Some(inner_schema) = schema + .as_object_mut() + .and_then(|o| o.get_mut("items")) + .and_then(|i| i.try_into().ok()) + { + f(inner_schema); + } +} + +pub fn flatten(schema: &mut Schema, other: Schema) { + fn flatten_property(obj1: &mut Map, key: String, value2: Value) { + match obj1.entry(key) { + Entry::Vacant(vacant) => { + vacant.insert(value2); + } + Entry::Occupied(occupied) => { + match occupied.key().as_str() { + "required" | "allOf" => { + if let Value::Array(a1) = occupied.into_mut() { + if let Value::Array(a2) = value2 { + a1.extend(a2); + } + } + } + "properties" | "patternProperties" => { + if let Value::Object(o1) = occupied.into_mut() { + if let Value::Object(o2) = value2 { + o1.extend(o2); + } + } + } + "oneOf" | "anyOf" => { + let (key, current) = occupied.remove_entry(); + flatten_property( + obj1, + "allOf".to_owned(), + json!([ + { &key: current }, + { key: value2 } + ]), + ); + } + _ => { + // leave the original value as it is (don't modify `schema`) + } + }; + } + } + } + + match other.try_to_object() { + Err(false) => {} + Err(true) => { + if let Some(obj) = schema.as_object_mut() { + if !obj.contains_key("additionalProperties") + && !obj.contains_key("unevaluatedProperties") + { + let key = if contains_immediate_subschema(obj) { + "unevaluatedProperties" + } else { + "additionalProperties" + }; + obj.insert(key.to_owned(), true.into()); + } + } + } + Ok(mut obj2) => { + let obj1 = schema.ensure_object(); + + // For complex merges, replace `additionalProperties` with `unevaluatedProperties` + // which usually "works out better". + normalise_additional_unevaluated_properties(obj1, &obj2); + normalise_additional_unevaluated_properties(&mut obj2, obj1); + + for (key, value2) in obj2 { + flatten_property(obj1, key, value2); + } + } + } +} + +fn normalise_additional_unevaluated_properties( + schema_obj1: &mut Map, + schema_obj2: &Map, +) { + if schema_obj1.contains_key("additionalProperties") + && (schema_obj2.contains_key("unevaluatedProperties") + || contains_immediate_subschema(schema_obj2)) + { + let ap = schema_obj1.remove("additionalProperties"); + schema_obj1.insert("unevaluatedProperties".to_owned(), ap.into()); + } +} + +fn contains_immediate_subschema(schema_obj: &Map) -> bool { + ["if", "then", "else", "allOf", "anyOf", "oneOf", "$ref"] + .into_iter() + .any(|k| schema_obj.contains_key(k)) +} diff --git a/schemars_derive/src/regex_syntax.rs b/schemars/src/_private/regex_syntax.rs similarity index 91% rename from schemars_derive/src/regex_syntax.rs rename to schemars/src/_private/regex_syntax.rs index 750d1e2b..23b67caf 100644 --- a/schemars_derive/src/regex_syntax.rs +++ b/schemars/src/_private/regex_syntax.rs @@ -1,5 +1,7 @@ +#![allow(clippy::all)] +use crate::_alloc_prelude::*; // 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/src/_private/rustdoc.rs b/schemars/src/_private/rustdoc.rs new file mode 100644 index 00000000..8fcdaca6 --- /dev/null +++ b/schemars/src/_private/rustdoc.rs @@ -0,0 +1,94 @@ +pub const fn get_title_and_description(doc: &str) -> (&str, &str) { + let doc_bytes = trim_ascii(doc.as_bytes()); + + if !doc_bytes.is_empty() && doc_bytes[0] == b'#' { + let title_end_index = match strchr(doc_bytes, b'\n') { + Some(i) => i, + None => doc_bytes.len(), + }; + + let title = trim_ascii(trim_start(subslice(doc_bytes, 0, title_end_index), b'#')); + let description = trim_ascii(subslice(doc_bytes, title_end_index, doc_bytes.len())); + + (to_utf8(title), to_utf8(description)) + } else { + ("", to_utf8(doc_bytes)) + } +} + +const fn strchr(bytes: &[u8], chr: u8) -> Option { + let len = bytes.len(); + let mut i = 0; + while i < len { + if bytes[i] == chr { + return Some(i); + } + i += 1; + } + None +} + +const fn subslice(mut bytes: &[u8], mut start: usize, end: usize) -> &[u8] { + let mut trim_end_count = bytes.len() - end; + if trim_end_count > 0 { + while let [rest @ .., _last] = bytes { + bytes = rest; + + trim_end_count -= 1; + if trim_end_count == 0 { + break; + } + } + } + + if start > 0 { + while let [_first, rest @ ..] = bytes { + bytes = rest; + + start -= 1; + if start == 0 { + break; + } + } + } + + bytes +} + +const fn to_utf8(bytes: &[u8]) -> &str { + match core::str::from_utf8(bytes) { + Ok(x) => x, + Err(_) => panic!("Invalid UTF-8"), + } +} + +const fn trim_start(mut bytes: &[u8], chr: u8) -> &[u8] { + while let [first, rest @ ..] = bytes { + if *first == chr { + bytes = rest; + } else { + break; + } + } + bytes +} + +const fn trim_ascii(mut bytes: &[u8]) -> &[u8] { + while let [first, rest @ ..] = bytes { + if first.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + + bytes +} diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs deleted file mode 100644 index 35a734fa..00000000 --- a/schemars/src/flatten.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::schema::*; -use crate::{Map, Set}; - -impl Schema { - /// This function is only public for use by schemars_derive. - /// - /// It should not be considered part of the public API. - #[doc(hidden)] - pub fn flatten(self, other: Self) -> Schema { - if is_null_type(&self) { - return other; - } else if is_null_type(&other) { - return self; - } - let s1: SchemaObject = self.into(); - let s2: SchemaObject = other.into(); - Schema::Object(s1.merge(s2)) - } -} - -pub(crate) trait Merge: Sized { - fn merge(self, other: Self) -> Self; -} - -macro_rules! impl_merge { - ($ty:ident { merge: $($merge_field:ident)*, or: $($or_field:ident)*, }) => { - impl Merge for $ty { - fn merge(self, other: Self) -> Self { - $ty { - $($merge_field: self.$merge_field.merge(other.$merge_field),)* - $($or_field: self.$or_field.or(other.$or_field),)* - } - } - } - }; - ($ty:ident { or: $($or_field:ident)*, }) => { - impl_merge!( $ty { merge: , or: $($or_field)*, }); - }; -} - -// For ObjectValidation::additional_properties. -impl Merge for Option> { - fn merge(self, other: Self) -> Self { - match (self.map(|x| *x), other.map(|x| *x)) { - // Perfer permissive schemas. - (Some(Schema::Bool(true)), _) => Some(Box::new(true.into())), - (_, Some(Schema::Bool(true))) => Some(Box::new(true.into())), - (None, _) => None, - (_, None) => None, - - // Merge if we have two non-trivial schemas. - (Some(Schema::Object(s1)), Some(Schema::Object(s2))) => { - Some(Box::new(Schema::Object(s1.merge(s2)))) - } - - // Perfer the more permissive schema. - (Some(s1 @ Schema::Object(_)), Some(Schema::Bool(false))) => Some(Box::new(s1)), - (Some(Schema::Bool(false)), Some(s2 @ Schema::Object(_))) => Some(Box::new(s2)), - - // Default to the null schema. - (Some(Schema::Bool(false)), Some(Schema::Bool(false))) => Some(Box::new(false.into())), - } - } -} - -impl_merge!(SchemaObject { - merge: extensions instance_type enum_values - metadata subschemas number string array object, - or: format const_value reference, -}); - -impl Merge for Metadata { - fn merge(self, other: Self) -> Self { - Metadata { - id: self.id.or(other.id), - title: self.title.or(other.title), - description: self.description.or(other.description), - default: self.default.or(other.default), - deprecated: self.deprecated || other.deprecated, - read_only: self.read_only || other.read_only, - write_only: self.write_only || other.write_only, - examples: self.examples.merge(other.examples), - } - } -} - -impl_merge!(SubschemaValidation { - or: all_of any_of one_of not if_schema then_schema else_schema, -}); - -impl_merge!(NumberValidation { - or: multiple_of maximum exclusive_maximum minimum exclusive_minimum, -}); - -impl_merge!(StringValidation { - or: max_length min_length pattern, -}); - -impl_merge!(ArrayValidation { - or: items additional_items max_items min_items unique_items contains, -}); - -impl_merge!(ObjectValidation { - merge: required properties pattern_properties additional_properties, - or: max_properties min_properties property_names, -}); - -impl Merge for Option { - fn merge(self, other: Self) -> Self { - match (self, other) { - (Some(x), Some(y)) => Some(x.merge(y)), - (None, y) => y, - (x, None) => x, - } - } -} - -impl Merge for Box { - fn merge(mut self, other: Self) -> Self { - *self = (*self).merge(*other); - self - } -} - -impl Merge for Vec { - fn merge(mut self, other: Self) -> Self { - self.extend(other); - self - } -} - -impl Merge for Map -where - K: std::hash::Hash + Eq + Ord, -{ - fn merge(mut self, other: Self) -> Self { - self.extend(other); - self - } -} - -impl Merge for Set { - fn merge(mut self, other: Self) -> Self { - self.extend(other); - self - } -} - -impl Merge for SingleOrVec { - fn merge(self, other: Self) -> Self { - if self == other { - return self; - } - let mut vec = match (self, other) { - (SingleOrVec::Vec(v1), SingleOrVec::Vec(v2)) => v1.merge(v2), - (SingleOrVec::Vec(mut v), SingleOrVec::Single(s)) - | (SingleOrVec::Single(s), SingleOrVec::Vec(mut v)) => { - v.push(*s); - v - } - (SingleOrVec::Single(s1), SingleOrVec::Single(s2)) => vec![*s1, *s2], - }; - vec.sort(); - vec.dedup(); - SingleOrVec::Vec(vec) - } -} - -fn is_null_type(schema: &Schema) -> bool { - let s = match schema { - Schema::Object(s) => s, - _ => return false, - }; - match &s.instance_type { - Some(SingleOrVec::Single(t)) if **t == InstanceType::Null => true, - _ => false, - } -} diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs deleted file mode 100644 index 58eaa61e..00000000 --- a/schemars/src/gen.rs +++ /dev/null @@ -1,486 +0,0 @@ -/*! -JSON Schema generator and settings. - -This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you. -There are two main types in this module: -* [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). -* [`SchemaGenerator`], which manages the generation of a schema document. -*/ - -use crate::schema::*; -use crate::{visit::*, JsonSchema, Map}; -use dyn_clone::DynClone; -use serde::Serialize; -use std::{any::Any, collections::HashSet, fmt::Debug}; - -/// Settings to customize how Schemas are generated. -/// -/// 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)] -pub struct SchemaSettings { - /// If `true`, schemas for [`Option`](Option) will include a `nullable` property. - /// - /// This is not part of the JSON Schema spec, but is used in Swagger/OpenAPI schemas. - /// - /// Defaults to `false`. - pub option_nullable: bool, - /// If `true`, schemas for [`Option`](Option) will have `null` added to their [`type`](../schema/struct.SchemaObject.html#structfield.instance_type). - /// - /// Defaults to `true`. - pub option_add_null_type: bool, - /// A JSON pointer to the expected location of referenceable subschemas within the resulting root schema. - /// - /// Defaults to `"#/definitions/"`. - pub definitions_path: String, - /// The URI of the meta-schema describing the structure of the generated schemas. - /// - /// Defaults to `"http://json-schema.org/draft-07/schema#"`. - pub meta_schema: Option, - /// A list of visitors that get applied to all generated root schemas. - pub visitors: Vec>, - /// Inline all subschemas instead of using references. - /// - /// Some references may still be generated in schemas for recursive types. - /// - /// Defaults to `false`. - pub inline_subschemas: bool, - _hidden: (), -} - -impl Default for SchemaSettings { - fn default() -> SchemaSettings { - SchemaSettings::draft07() - } -} - -impl SchemaSettings { - /// Creates `SchemaSettings` that conform to [JSON Schema Draft 7](https://json-schema.org/specification-links.html#draft-7). - pub fn draft07() -> SchemaSettings { - SchemaSettings { - option_nullable: false, - option_add_null_type: true, - definitions_path: "#/definitions/".to_owned(), - meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()), - visitors: vec![Box::new(RemoveRefSiblings)], - inline_subschemas: false, - _hidden: (), - } - } - - /// Creates `SchemaSettings` that conform to [JSON Schema 2019-09](https://json-schema.org/specification-links.html#2019-09-formerly-known-as-draft-8). - pub fn draft2019_09() -> SchemaSettings { - SchemaSettings { - option_nullable: false, - option_add_null_type: true, - definitions_path: "#/definitions/".to_owned(), - meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()), - visitors: Vec::default(), - inline_subschemas: false, - _hidden: (), - } - } - - /// Creates `SchemaSettings` that conform to [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject). - pub fn openapi3() -> SchemaSettings { - SchemaSettings { - option_nullable: true, - option_add_null_type: false, - definitions_path: "#/components/schemas/".to_owned(), - meta_schema: Some( - "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema" - .to_owned(), - ), - visitors: vec![ - Box::new(RemoveRefSiblings), - Box::new(ReplaceBoolSchemas { - skip_additional_properties: true, - }), - Box::new(SetSingleExample { - retain_examples: false, - }), - ], - inline_subschemas: false, - _hidden: (), - } - } - - /// Modifies the `SchemaSettings` by calling the given function. - /// - /// # Example - /// ``` - /// use schemars::gen::{SchemaGenerator, SchemaSettings}; - /// - /// let settings = SchemaSettings::default().with(|s| { - /// s.option_nullable = true; - /// s.option_add_null_type = false; - /// }); - /// let gen = settings.into_generator(); - /// ``` - pub fn with(mut self, configure_fn: impl FnOnce(&mut Self)) -> Self { - configure_fn(&mut self); - self - } - - /// Appends the given visitor to the list of [visitors](SchemaSettings::visitors) for these `SchemaSettings`. - pub fn with_visitor(mut self, visitor: impl Visitor + Debug + Clone + 'static) -> Self { - self.visitors.push(Box::new(visitor)); - self - } - - /// Creates a new [`SchemaGenerator`] using these settings. - pub fn into_generator(self) -> SchemaGenerator { - SchemaGenerator::new(self) - } -} - -/// The main type used to generate JSON Schemas. -/// -/// # Example -/// ``` -/// use schemars::{JsonSchema, gen::SchemaGenerator}; -/// -/// #[derive(JsonSchema)] -/// struct MyStruct { -/// foo: i32, -/// } -/// -/// let gen = SchemaGenerator::default(); -/// let schema = gen.into_root_schema_for::(); -/// ``` -#[derive(Debug, Default)] -pub struct SchemaGenerator { - settings: SchemaSettings, - definitions: Map, - pending_schema_names: HashSet, -} - -impl Clone for SchemaGenerator { - fn clone(&self) -> Self { - Self { - settings: self.settings.clone(), - definitions: self.definitions.clone(), - pending_schema_names: HashSet::new(), - } - } -} - -impl From for SchemaGenerator { - fn from(settings: SchemaSettings) -> Self { - settings.into_generator() - } -} - -impl SchemaGenerator { - /// Creates a new `SchemaGenerator` using the given settings. - pub fn new(settings: SchemaSettings) -> SchemaGenerator { - SchemaGenerator { - settings, - ..Default::default() - } - } - - /// Borrows the [`SchemaSettings`] being used by this `SchemaGenerator`. - /// - /// # Example - /// ``` - /// use schemars::gen::SchemaGenerator; - /// - /// let gen = SchemaGenerator::default(); - /// let settings = gen.settings(); - /// - /// assert_eq!(settings.option_add_null_type, true); - /// ``` - pub fn settings(&self) -> &SchemaSettings { - &self.settings - } - - #[deprecated = "This method no longer has any effect."] - pub fn make_extensible(&self, _schema: &mut SchemaObject) {} - - #[deprecated = "Use `Schema::Bool(true)` instead"] - pub fn schema_for_any(&self) -> Schema { - Schema::Bool(true) - } - - #[deprecated = "Use `Schema::Bool(false)` instead"] - pub fn schema_for_none(&self) -> Schema { - Schema::Bool(false) - } - - /// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` schema referencing `T`'s schema. - /// - /// If `T` is [referenceable](JsonSchema::is_referenceable), this will add `T`'s schema to this generator's definitions, and - /// return a `$ref` schema referencing that schema. Otherwise, this method behaves identically to [`JsonSchema::json_schema`]. - /// - /// 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 return_ref = T::is_referenceable() - && (!self.settings.inline_subschemas || self.pending_schema_names.contains(&name)); - - if return_ref { - let reference = format!("{}{}", self.settings().definitions_path, name); - if !self.definitions.contains_key(&name) { - self.insert_new_subschema_for::(name); - } - Schema::new_ref(reference) - } else { - self.json_schema_internal::(&name) - } - } - - fn insert_new_subschema_for(&mut self, name: String) { - 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); - - self.definitions.insert(name, schema); - } - - /// Borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated. - /// - /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas - /// themselves. - pub fn definitions(&self) -> &Map { - &self.definitions - } - - /// Mutably borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated. - /// - /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas - /// themselves. - pub fn definitions_mut(&mut self) -> &mut Map { - &mut self.definitions - } - - /// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated, - /// leaving an empty map in its place. - /// - /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas - /// themselves. - pub fn take_definitions(&mut self) -> Map { - std::mem::take(&mut self.definitions) - } - - /// Returns an iterator over the [visitors](SchemaSettings::visitors) being used by this `SchemaGenerator`. - pub fn visitors_mut(&mut self) -> impl Iterator { - self.settings.visitors.iter_mut().map(|v| v.as_mut()) - } - - /// Generates a root JSON Schema for the type `T`. - /// - /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will - /// 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 root = RootSchema { - meta_schema: self.settings.meta_schema.clone(), - definitions: self.definitions.clone(), - schema, - }; - - for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) - } - - root - } - - /// Consumes `self` and generates a root JSON Schema for the type `T`. - /// - /// 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 root = RootSchema { - meta_schema: self.settings.meta_schema, - definitions: self.definitions, - schema, - }; - - for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) - } - - root - } - - /// Generates a root JSON Schema for the given example value. - /// - /// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`root_schema_for()`](Self::root_schema_for()) - /// function which will generally produce a more precise schema, particularly when the value contains any enums. - pub fn root_schema_for_value( - &mut self, - value: &T, - ) -> Result { - let mut schema = value - .serialize(crate::ser::Serializer { - gen: self, - include_title: true, - })? - .into_object(); - - if let Ok(example) = serde_json::to_value(value) { - schema.metadata().examples.push(example); - } - - let mut root = RootSchema { - meta_schema: self.settings.meta_schema.clone(), - definitions: self.definitions.clone(), - schema, - }; - - for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) - } - - Ok(root) - } - - /// Consumes `self` and generates a root JSON Schema for the given example value. - /// - /// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`into_root_schema_for()!`](Self::into_root_schema_for()) - /// function which will generally produce a more precise schema, particularly when the value contains any enums. - pub fn into_root_schema_for_value( - mut self, - value: &T, - ) -> Result { - let mut schema = value - .serialize(crate::ser::Serializer { - gen: &mut self, - include_title: true, - })? - .into_object(); - - if let Ok(example) = serde_json::to_value(value) { - schema.metadata().examples.push(example); - } - - let mut root = RootSchema { - meta_schema: self.settings.meta_schema, - definitions: self.definitions, - schema, - }; - - for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) - } - - Ok(root) - } - - /// Attemps to find the schema that the given `schema` is referencing. - /// - /// If the given `schema` has a [`$ref`](../schema/struct.SchemaObject.html#structfield.reference) property which refers - /// to another schema in `self`'s schema definitions, the referenced schema will be returned. Otherwise, returns `None`. - /// - /// # Example - /// ``` - /// use schemars::{JsonSchema, gen::SchemaGenerator}; - /// - /// #[derive(JsonSchema)] - /// struct MyStruct { - /// foo: i32, - /// } - /// - /// let mut gen = SchemaGenerator::default(); - /// let ref_schema = gen.subschema_for::(); - /// - /// assert!(ref_schema.is_ref()); - /// - /// let dereferenced = gen.dereference(&ref_schema); - /// - /// assert!(dereferenced.is_some()); - /// assert!(!dereferenced.unwrap().is_ref()); - /// assert_eq!(dereferenced, gen.definitions().get("MyStruct")); - /// ``` - pub fn dereference<'a>(&'a self, schema: &Schema) -> Option<&'a Schema> { - match schema { - Schema::Object(SchemaObject { - reference: Some(ref schema_ref), - .. - }) => { - let definitions_path = &self.settings().definitions_path; - if schema_ref.starts_with(definitions_path) { - let name = &schema_ref[definitions_path.len()..]; - self.definitions.get(name) - } else { - None - } - } - _ => None, - } - } - - fn json_schema_internal(&mut self, name: &str) -> Schema { - struct PendingSchemaState<'a> { - gen: &'a mut SchemaGenerator, - name: &'a 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 } - } - } - - impl Drop for PendingSchemaState<'_> { - fn drop(&mut self) { - if self.did_add { - self.gen.pending_schema_names.remove(self.name); - } - } - } - - let pss = PendingSchemaState::new(self, name); - T::json_schema(pss.gen) - } -} - -/// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings]. -/// -/// You will rarely need to use this trait directly as it is automatically implemented for any type which implements all of: -/// - [`Visitor`] -/// - [`std::fmt::Debug`] -/// - [`std::any::Any`] (implemented for all `'static` types) -/// - [`std::clone::Clone`] -/// -/// # Example -/// ``` -/// use schemars::visit::Visitor; -/// use schemars::gen::GenVisitor; -/// -/// #[derive(Debug, Clone)] -/// struct MyVisitor; -/// -/// impl Visitor for MyVisitor { } -/// -/// let v: &dyn GenVisitor = &MyVisitor; -/// assert!(v.as_any().is::()); -/// ``` -pub trait GenVisitor: Visitor + Debug + DynClone + Any { - /// Upcasts this visitor into an `Any`, which can be used to inspect and manipulate it as its concrete type. - fn as_any(&self) -> &dyn Any; -} - -dyn_clone::clone_trait_object!(GenVisitor); - -impl GenVisitor for T -where - T: Visitor + Debug + Clone + Any, -{ - fn as_any(&self) -> &dyn Any { - self - } -} diff --git a/schemars/src/generate.rs b/schemars/src/generate.rs new file mode 100644 index 00000000..e51ee66a --- /dev/null +++ b/schemars/src/generate.rs @@ -0,0 +1,620 @@ +/*! +JSON Schema generator and settings. + +This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you. +There are two main types in this module: +* [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). +* [`SchemaGenerator`], which manages the generation of a schema document. +*/ + +use crate::Schema; +use crate::_alloc_prelude::*; +use crate::{transform::*, JsonSchema}; +use alloc::collections::{BTreeMap, BTreeSet}; +use core::{any::Any, fmt::Debug}; +use dyn_clone::DynClone; +use serde::Serialize; +use serde_json::{Map as JsonMap, Value}; + +type CowStr = alloc::borrow::Cow<'static, str>; + +/// Settings to customize how Schemas are generated. +/// +/// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. +/// If you rely on generated schemas conforming to draft 2020-12, consider using the +/// [`SchemaSettings::draft2020_12()`] method. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct SchemaSettings { + /// If `true`, schemas for [`Option`] will include a `nullable` property. + /// + /// This is not part of the JSON Schema spec, but is used in Swagger/OpenAPI schemas. + /// + /// Defaults to `false`. + pub option_nullable: bool, + /// If `true`, schemas for [`Option`] will have `null` added to their `type` property. + /// + /// Defaults to `true`. + pub option_add_null_type: bool, + /// A JSON pointer to the expected location of referenceable subschemas within the resulting + /// root schema. + /// + /// A single leading `#` and/or single trailing `/` are ignored. + /// + /// Defaults to `"/$defs"`. + pub definitions_path: String, + /// The URI of the meta-schema describing the structure of the generated schemas. + /// + /// Defaults to `"https://json-schema.org/draft/2020-12/schema"`. + pub meta_schema: Option, + /// A list of [`Transform`]s that get applied to generated root schemas. + pub transforms: Vec>, + /// Inline all subschemas instead of using references. + /// + /// Some references may still be generated in schemas for recursive types. + /// + /// Defaults to `false`. + pub inline_subschemas: bool, +} + +impl Default for SchemaSettings { + /// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. + /// If you rely on generated schemas conforming to draft 2020-12, consider using the + /// [`SchemaSettings::draft2020_12()`] method. + fn default() -> SchemaSettings { + SchemaSettings::draft2020_12() + } +} + +impl SchemaSettings { + /// Creates `SchemaSettings` that conform to [JSON Schema Draft 7](https://json-schema.org/specification-links#draft-7). + pub fn draft07() -> SchemaSettings { + SchemaSettings { + option_nullable: false, + option_add_null_type: true, + definitions_path: "/definitions".to_owned(), + meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()), + transforms: vec![ + Box::new(ReplaceUnevaluatedProperties), + Box::new(RemoveRefSiblings), + Box::new(ReplacePrefixItems), + ], + inline_subschemas: false, + } + } + + /// Creates `SchemaSettings` that conform to [JSON Schema 2019-09](https://json-schema.org/specification-links#draft-2019-09-(formerly-known-as-draft-8)). + pub fn draft2019_09() -> SchemaSettings { + SchemaSettings { + option_nullable: false, + option_add_null_type: true, + definitions_path: "/$defs".to_owned(), + meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()), + transforms: vec![Box::new(ReplacePrefixItems)], + inline_subschemas: false, + } + } + + /// Creates `SchemaSettings` that conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12). + pub fn draft2020_12() -> SchemaSettings { + SchemaSettings { + option_nullable: false, + option_add_null_type: true, + definitions_path: "/$defs".to_owned(), + meta_schema: Some("https://json-schema.org/draft/2020-12/schema".to_owned()), + transforms: Vec::new(), + inline_subschemas: false, + } + } + + /// Creates `SchemaSettings` that conform to [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema). + pub fn openapi3() -> SchemaSettings { + SchemaSettings { + option_nullable: true, + option_add_null_type: false, + definitions_path: "/components/schemas".to_owned(), + meta_schema: Some( + "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema" + .to_owned(), + ), + transforms: vec![ + Box::new(ReplaceUnevaluatedProperties), + Box::new(RemoveRefSiblings), + Box::new(ReplaceBoolSchemas { + skip_additional_properties: true, + }), + Box::new(SetSingleExample), + Box::new(ReplaceConstValue), + Box::new(ReplacePrefixItems), + ], + inline_subschemas: false, + } + } + + /// Modifies the `SchemaSettings` by calling the given function. + /// + /// # Example + /// ``` + /// use schemars::generate::{SchemaGenerator, SchemaSettings}; + /// + /// let settings = SchemaSettings::default().with(|s| { + /// s.option_nullable = true; + /// s.option_add_null_type = false; + /// }); + /// let generator = settings.into_generator(); + /// ``` + pub fn with(mut self, configure_fn: impl FnOnce(&mut Self)) -> Self { + configure_fn(&mut self); + self + } + + /// Appends the given transform to the list of [transforms](SchemaSettings::transforms) for + /// these `SchemaSettings`. + pub fn with_transform(mut self, transform: impl Transform + Clone + 'static + Send) -> Self { + self.transforms.push(Box::new(transform)); + self + } + + /// Creates a new [`SchemaGenerator`] using these settings. + pub fn into_generator(self) -> SchemaGenerator { + SchemaGenerator::new(self) + } +} + +/// The main type used to generate JSON Schemas. +/// +/// # Example +/// ``` +/// use schemars::{JsonSchema, SchemaGenerator}; +/// +/// #[derive(JsonSchema)] +/// struct MyStruct { +/// foo: i32, +/// } +/// +/// let generator = SchemaGenerator::default(); +/// let schema = generator.into_root_schema_for::(); +/// ``` +#[derive(Debug, Default)] +pub struct SchemaGenerator { + settings: SchemaSettings, + definitions: JsonMap, + pending_schema_ids: BTreeSet, + schema_id_to_name: BTreeMap, + used_schema_names: BTreeSet, +} + +impl Clone for SchemaGenerator { + fn clone(&self) -> Self { + Self { + settings: self.settings.clone(), + definitions: self.definitions.clone(), + pending_schema_ids: BTreeSet::new(), + schema_id_to_name: BTreeMap::new(), + used_schema_names: BTreeSet::new(), + } + } +} + +impl From for SchemaGenerator { + fn from(settings: SchemaSettings) -> Self { + settings.into_generator() + } +} + +impl SchemaGenerator { + /// Creates a new `SchemaGenerator` using the given settings. + pub fn new(settings: SchemaSettings) -> SchemaGenerator { + SchemaGenerator { + settings, + ..Default::default() + } + } + + /// Borrows the [`SchemaSettings`] being used by this `SchemaGenerator`. + /// + /// # Example + /// ``` + /// use schemars::SchemaGenerator; + /// + /// let generator = SchemaGenerator::default(); + /// let settings = generator.settings(); + /// + /// assert_eq!(settings.option_add_null_type, true); + /// ``` + pub fn settings(&self) -> &SchemaSettings { + &self.settings + } + + /// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` + /// schema referencing `T`'s schema. + /// + /// If `T` is not [inlined](JsonSchema::always_inline_schema), this will add `T`'s schema to + /// this generator's definitions, and return a `$ref` schema referencing that schema. + /// Otherwise, this method behaves identically to [`JsonSchema::json_schema`]. + /// + /// If `T`'s schema depends on any [non-inlined](JsonSchema::always_inline_schema) schemas, then + /// this method will add them to the `SchemaGenerator`'s schema definitions. + pub fn subschema_for(&mut self) -> Schema { + let id = T::schema_id(); + let return_ref = !T::always_inline_schema() + && (!self.settings.inline_subschemas || self.pending_schema_ids.contains(&id)); + + if return_ref { + let name = match self.schema_id_to_name.get(&id).cloned() { + Some(n) => n, + None => { + let base_name = T::schema_name(); + let mut name = CowStr::Borrowed(""); + + if self.used_schema_names.contains(base_name.as_ref()) { + for i in 2.. { + name = format!("{base_name}{i}").into(); + 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.definitions_path_stripped(), name); + if !self.definitions.contains_key(name.as_ref()) { + self.insert_new_subschema_for::(name, id); + } + Schema::new_ref(reference) + } else { + self.json_schema_internal::(id) + } + } + + fn insert_new_subschema_for(&mut self, name: CowStr, id: CowStr) { + let dummy = false.into(); + // insert into definitions BEFORE calling json_schema to avoid infinite recursion + self.definitions.insert(name.clone().into(), dummy); + + let schema = self.json_schema_internal::(id); + + self.definitions.insert(name.into(), schema.to_value()); + } + + /// Borrows the collection of all [non-inlined](JsonSchema::always_inline_schema) schemas that + /// have been generated. + /// + /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the + /// values are the schemas themselves. + pub fn definitions(&self) -> &JsonMap { + &self.definitions + } + + /// Mutably borrows the collection of all [non-inlined](JsonSchema::always_inline_schema) + /// schemas that have been generated. + /// + /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the + /// values are the schemas themselves. + pub fn definitions_mut(&mut self) -> &mut JsonMap { + &mut self.definitions + } + + /// Returns the collection of all [non-inlined](JsonSchema::always_inline_schema) schemas that + /// have been generated, leaving an empty `Map` in its place. + /// + /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the + /// values are the schemas themselves. + pub fn take_definitions(&mut self) -> JsonMap { + core::mem::take(&mut self.definitions) + } + + /// Returns an iterator over the [transforms](SchemaSettings::transforms) being used by this + /// `SchemaGenerator`. + pub fn transforms_mut(&mut self) -> impl Iterator { + self.settings.transforms.iter_mut().map(Box::as_mut) + } + + /// Generates a JSON Schema for the type `T`. + /// + /// If `T`'s schema depends on any [non-inlined](JsonSchema::always_inline_schema) schemas, then + /// this method will include them in the returned `Schema` at the [definitions + /// path](SchemaSettings::definitions_path) (by default `"$defs"`). + pub fn root_schema_for(&mut self) -> Schema { + let mut schema = self.json_schema_internal::(T::schema_id()); + + let object = schema.ensure_object(); + + object + .entry("title") + .or_insert_with(|| T::schema_name().into()); + + if let Some(meta_schema) = self.settings.meta_schema.as_deref() { + object.insert("$schema".into(), meta_schema.into()); + } + + self.add_definitions(object, self.definitions.clone()); + self.apply_transforms(&mut schema); + + schema + } + + /// Consumes `self` and generates a JSON Schema for the type `T`. + /// + /// If `T`'s schema depends on any [non-inlined](JsonSchema::always_inline_schema) schemas, then + /// this method will include them in the returned `Schema` at the [definitions + /// path](SchemaSettings::definitions_path) (by default `"$defs"`). + pub fn into_root_schema_for(mut self) -> Schema { + let mut schema = self.json_schema_internal::(T::schema_id()); + + let object = schema.ensure_object(); + + object + .entry("title") + .or_insert_with(|| T::schema_name().into()); + + if let Some(meta_schema) = core::mem::take(&mut self.settings.meta_schema) { + object.insert("$schema".into(), meta_schema.into()); + } + + let definitions = self.take_definitions(); + self.add_definitions(object, definitions); + self.apply_transforms(&mut schema); + + schema + } + + /// Generates a JSON Schema for the given example value. + /// + /// If the value implements [`JsonSchema`], then prefer using the + /// [`root_schema_for()`](Self::root_schema_for()) function which will generally produce a + /// more precise schema, particularly when the value contains any enums. + /// + /// If the `Serialize` implementation of the value decides to fail, this will return an [`Err`]. + pub fn root_schema_for_value( + &mut self, + value: &T, + ) -> Result { + let mut schema = value.serialize(crate::ser::Serializer { + generator: self, + include_title: true, + })?; + + let object = schema.ensure_object(); + + if let Ok(example) = serde_json::to_value(value) { + object.insert("examples".into(), vec![example].into()); + } + + if let Some(meta_schema) = self.settings.meta_schema.as_deref() { + object.insert("$schema".into(), meta_schema.into()); + } + + self.add_definitions(object, self.definitions.clone()); + self.apply_transforms(&mut schema); + + Ok(schema) + } + + /// Consumes `self` and generates a JSON Schema for the given example value. + /// + /// If the value implements [`JsonSchema`], then prefer using the + /// [`into_root_schema_for()!`](Self::into_root_schema_for()) function which will generally + /// produce a more precise schema, particularly when the value contains any enums. + /// + /// If the `Serialize` implementation of the value decides to fail, this will return an [`Err`]. + pub fn into_root_schema_for_value( + mut self, + value: &T, + ) -> Result { + let mut schema = value.serialize(crate::ser::Serializer { + generator: &mut self, + include_title: true, + })?; + + let object = schema.ensure_object(); + + if let Ok(example) = serde_json::to_value(value) { + object.insert("examples".into(), vec![example].into()); + } + + if let Some(meta_schema) = core::mem::take(&mut self.settings.meta_schema) { + object.insert("$schema".into(), meta_schema.into()); + } + + let definitions = self.take_definitions(); + self.add_definitions(object, definitions); + self.apply_transforms(&mut schema); + + Ok(schema) + } + + fn json_schema_internal(&mut self, id: CowStr) -> Schema { + struct PendingSchemaState<'a> { + generator: &'a mut SchemaGenerator, + id: CowStr, + did_add: bool, + } + + impl<'a> PendingSchemaState<'a> { + fn new(generator: &'a mut SchemaGenerator, id: CowStr) -> Self { + let did_add = generator.pending_schema_ids.insert(id.clone()); + Self { + generator, + id, + did_add, + } + } + } + + impl Drop for PendingSchemaState<'_> { + fn drop(&mut self) { + if self.did_add { + self.generator.pending_schema_ids.remove(&self.id); + } + } + } + + let pss = PendingSchemaState::new(self, id); + T::json_schema(pss.generator) + } + + fn add_definitions( + &mut self, + schema_object: &mut JsonMap, + mut definitions: JsonMap, + ) { + if definitions.is_empty() { + return; + } + + let pointer = self.definitions_path_stripped(); + let Some(target) = json_pointer_mut(schema_object, pointer, true) else { + return; + }; + + target.append(&mut definitions); + } + + fn apply_transforms(&mut self, schema: &mut Schema) { + for transform in self.transforms_mut() { + transform.transform(schema); + } + } + + /// Returns `self.settings.definitions_path` as a plain JSON pointer to the definitions object, + /// i.e. without a leading '#' or trailing '/' + fn definitions_path_stripped(&self) -> &str { + let path = &self.settings.definitions_path; + let path = path.strip_prefix('#').unwrap_or(path); + path.strip_suffix('/').unwrap_or(path) + } +} + +fn json_pointer_mut<'a>( + mut object: &'a mut JsonMap, + pointer: &str, + create_if_missing: bool, +) -> Option<&'a mut JsonMap> { + use serde_json::map::Entry; + + let pointer = pointer.strip_prefix('/')?; + if pointer.is_empty() { + return Some(object); + } + + for mut segment in pointer.split('/') { + let replaced: String; + if segment.contains('~') { + replaced = segment.replace("~1", "/").replace("~0", "~"); + segment = &replaced; + } + + let next_value = match object.entry(segment) { + Entry::Occupied(o) => o.into_mut(), + Entry::Vacant(v) if create_if_missing => v.insert(Value::Object(JsonMap::default())), + Entry::Vacant(_) => return None, + }; + + object = next_value.as_object_mut()?; + } + + Some(object) +} + +/// A [`Transform`] which implements additional traits required to be included in a +/// [`SchemaSettings`]. +/// +/// You will rarely need to use this trait directly as it is automatically implemented for any type +/// which implements all of: +/// - [`Transform`] +/// - [`std::any::Any`] (implemented for all `'static` types) +/// - [`std::clone::Clone`] +/// - [`std::marker::Send`] +/// +/// # Example +/// ``` +/// use schemars::transform::Transform; +/// use schemars::generate::GenTransform; +/// +/// #[derive(Debug, Clone)] +/// struct MyTransform; +/// +/// impl Transform for MyTransform { +/// fn transform(&mut self, schema: &mut schemars::Schema) { +/// todo!() +/// } +/// } +/// +/// let v: &dyn GenTransform = &MyTransform; +/// assert!(v.as_any().is::()); +/// ``` +pub trait GenTransform: Transform + DynClone + Any + Send { + /// Upcasts this transform into an [`Any`], which can be used to inspect and manipulate it as + /// its concrete type. + /// + /// # Example + /// To remove a specific transform from an instance of `SchemaSettings`: + /// ``` + /// use schemars::generate::SchemaSettings; + /// use schemars::transform::ReplaceBoolSchemas; + /// + /// let mut settings = SchemaSettings::openapi3(); + /// let original_len = settings.transforms.len(); + /// + /// settings + /// .transforms + /// .retain(|t| !t.as_any().is::()); + /// + /// assert_eq!(settings.transforms.len(), original_len - 1); + /// ``` + fn as_any(&self) -> &dyn Any; + + /// Mutably upcasts this transform into an [`Any`], which can be used to inspect and manipulate + /// it as its concrete type. + /// + /// # Example + /// To modify a specific transform in an instance of `SchemaSettings`: + /// ``` + /// use schemars::generate::SchemaSettings; + /// use schemars::transform::ReplaceBoolSchemas; + /// + /// let mut settings = SchemaSettings::openapi3(); + /// for t in &mut settings.transforms { + /// if let Some(replace_bool_schemas) = t.as_any_mut().downcast_mut::() { + /// replace_bool_schemas.skip_additional_properties = false; + /// } + /// } + /// ``` + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +dyn_clone::clone_trait_object!(GenTransform); + +impl GenTransform for T +where + T: Transform + Clone + Any + Send, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Debug for Box { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self._debug_type_name(f) + } +} + +fn _assert_send() { + fn _assert() {} + + _assert::(); + _assert::(); +} diff --git a/schemars/src/json_schema_impls/array.rs b/schemars/src/json_schema_impls/array.rs index 558ddbd9..c17fbba1 100644 --- a/schemars/src/json_schema_impls/array.rs +++ b/schemars/src/json_schema_impls/array.rs @@ -1,25 +1,25 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; // Does not require T: JsonSchema. impl JsonSchema for [T; 0] { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - "EmptyArray".to_owned() + fn schema_name() -> Cow<'static, str> { + "EmptyArray".into() + } + + fn schema_id() -> Cow<'static, str> { + "[]".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - max_items: Some(0), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "maxItems": 0, + }) } } @@ -27,24 +27,23 @@ macro_rules! array_impls { ($($len:tt)+) => { $( impl JsonSchema for [T; $len] { - no_ref_schema!(); + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + format!("Array_size_{}_of_{}", $len, T::schema_name()).into() + } - fn schema_name() -> String { - format!("Array_size_{}_of_{}", $len, T::schema_name()) + fn schema_id() -> Cow<'static, str> { + format!("[{}; {}]", $len, T::schema_id()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: Some($len), - min_items: Some($len), - ..Default::default() - })), - ..Default::default() - } - .into() + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "items": serde_json::Value::from(generator.subschema_for::()), + "minItems": $len, + "maxItems": $len, + }) } } )+ @@ -57,40 +56,3 @@ array_impls! { 21 22 23 24 25 26 27 28 29 30 31 32 } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_array() { - let schema = schema_object_for::<[i32; 8]>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!( - array_validation.items, - Some(SingleOrVec::from(schema_for::())) - ); - assert_eq!(array_validation.max_items, Some(8)); - assert_eq!(array_validation.min_items, Some(8)); - } - - // SomeStruct does not implement JsonSchema - struct SomeStruct; - - #[test] - fn schema_for_empty_array() { - let schema = schema_object_for::<[SomeStruct; 0]>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!(array_validation.max_items, Some(0)); - } -} diff --git a/schemars/src/json_schema_impls/arrayvec05.rs b/schemars/src/json_schema_impls/arrayvec05.rs deleted file mode 100644 index 281bde79..00000000 --- a/schemars/src/json_schema_impls/arrayvec05.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use arrayvec05::{Array, ArrayString, ArrayVec}; -use std::convert::TryInto; - -// Do not set maxLength on the schema as that describes length in characters, but we only -// know max length in bytes. -forward_impl!(( JsonSchema for ArrayString where A: Array + Copy) => String); - -impl JsonSchema for ArrayVec -where - A::Item: JsonSchema, -{ - no_ref_schema!(); - - fn schema_name() -> String { - format!( - "Array_up_to_size_{}_of_{}", - A::CAPACITY, - A::Item::schema_name() - ) - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: A::CAPACITY.try_into().ok(), - ..Default::default() - })), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/arrayvec07.rs b/schemars/src/json_schema_impls/arrayvec07.rs index e2d92c5b..0d7270ff 100644 --- a/schemars/src/json_schema_impls/arrayvec07.rs +++ b/schemars/src/json_schema_impls/arrayvec07.rs @@ -1,8 +1,7 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; use arrayvec07::{ArrayString, ArrayVec}; -use std::convert::TryInto; // Do not set maxLength on the schema as that describes length in characters, but we only // know max length in bytes. @@ -12,22 +11,17 @@ impl JsonSchema for ArrayVec where T: JsonSchema, { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - format!("Array_up_to_size_{}_of_{}", CAP, T::schema_name()) + fn schema_name() -> alloc::borrow::Cow<'static, str> { + format!("Array_up_to_size_{}_of_{}", CAP, T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: CAP.try_into().ok(), - ..Default::default() - })), - ..Default::default() - } - .into() + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "items": generator.subschema_for::(), + "maxItems": CAP + }) } } diff --git a/schemars/src/json_schema_impls/atomic.rs b/schemars/src/json_schema_impls/atomic.rs index 5d8f6d91..e871e761 100644 --- a/schemars/src/json_schema_impls/atomic.rs +++ b/schemars/src/json_schema_impls/atomic.rs @@ -1,33 +1,47 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use std::sync::atomic::*; +use core::sync::atomic::*; +#[cfg(target_has_atomic = "8")] forward_impl!(AtomicBool => bool); +#[cfg(target_has_atomic = "8")] forward_impl!(AtomicI8 => i8); + +#[cfg(target_has_atomic = "16")] forward_impl!(AtomicI16 => i16); + +#[cfg(target_has_atomic = "32")] forward_impl!(AtomicI32 => i32); -#[cfg(std_atomic64)] + +#[cfg(target_has_atomic = "64")] forward_impl!(AtomicI64 => i64); + +#[cfg(target_has_atomic = "ptr")] forward_impl!(AtomicIsize => isize); +#[cfg(target_has_atomic = "8")] forward_impl!(AtomicU8 => u8); + +#[cfg(target_has_atomic = "16")] forward_impl!(AtomicU16 => u16); + +#[cfg(target_has_atomic = "32")] forward_impl!(AtomicU32 => u32); -#[cfg(std_atomic64)] + +#[cfg(target_has_atomic = "64")] forward_impl!(AtomicU64 => u64); + +#[cfg(target_has_atomic = "ptr")] forward_impl!(AtomicUsize => usize); #[cfg(test)] mod tests { use super::*; - use crate::tests::schema_object_for; + use crate::schema_for; use pretty_assertions::assert_eq; #[test] fn schema_for_atomics() { - let atomic_schema = schema_object_for::<( + let atomic_schema = schema_for!(( AtomicBool, AtomicI8, AtomicI16, @@ -39,9 +53,8 @@ mod tests { AtomicU32, AtomicU64, AtomicUsize, - )>(); - let basic_schema = - schema_object_for::<(bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)>(); + )); + let basic_schema = schema_for!((bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)); assert_eq!(atomic_schema, basic_schema); } } diff --git a/schemars/src/json_schema_impls/bytes.rs b/schemars/src/json_schema_impls/bytes.rs deleted file mode 100644 index f1a0f290..00000000 --- a/schemars/src/json_schema_impls/bytes.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use bytes::{Bytes, BytesMut}; - -forward_impl!((JsonSchema for Bytes) => Vec); -forward_impl!((JsonSchema for BytesMut) => Vec); diff --git a/schemars/src/json_schema_impls/chrono.rs b/schemars/src/json_schema_impls/chrono.rs deleted file mode 100644 index 005c89b3..00000000 --- a/schemars/src/json_schema_impls/chrono.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use chrono::prelude::*; -use serde_json::json; - -impl JsonSchema for Weekday { - no_ref_schema!(); - - fn schema_name() -> String { - "Weekday".to_owned() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![ - json!("Mon"), - json!("Tue"), - json!("Wed"), - json!("Thu"), - json!("Fri"), - json!("Sat"), - json!("Sun"), - ]), - ..Default::default() - } - .into() - } -} - -macro_rules! formatted_string_impl { - ($ty:ident, $format:literal) => { - formatted_string_impl!($ty, $format, JsonSchema for $ty); - }; - ($ty:ident, $format:literal, $($desc:tt)+) => { - impl $($desc)+ { - no_ref_schema!(); - - fn schema_name() -> String { - stringify!($ty).to_owned() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some($format.to_owned()), - ..Default::default() - } - .into() - } - } - }; -} - -formatted_string_impl!(NaiveDate, "date"); -formatted_string_impl!(NaiveDateTime, "partial-date-time"); -formatted_string_impl!(NaiveTime, "partial-date-time"); -formatted_string_impl!(DateTime, "date-time", JsonSchema for DateTime); diff --git a/schemars/src/json_schema_impls/chrono04.rs b/schemars/src/json_schema_impls/chrono04.rs new file mode 100644 index 00000000..a153aca3 --- /dev/null +++ b/schemars/src/json_schema_impls/chrono04.rs @@ -0,0 +1,62 @@ +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use chrono04::prelude::*; + +impl JsonSchema for Weekday { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + "Weekday".into() + } + + fn schema_id() -> Cow<'static, str> { + "chrono::Weekday".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + "enum": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + ] + }) + } +} + +macro_rules! formatted_string_impl { + ($ty:ident, $format:literal) => { + formatted_string_impl!($ty, $format, JsonSchema for $ty); + }; + ($ty:ident, $format:literal, $($desc:tt)+) => { + impl $($desc)+ { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + stringify!($ty).into() + } + + fn schema_id() -> Cow<'static, str> { + stringify!(chrono::$ty).into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + "format": $format + }) + } + } + }; +} + +formatted_string_impl!(NaiveDate, "date"); +formatted_string_impl!(NaiveDateTime, "partial-date-time"); +formatted_string_impl!(NaiveTime, "partial-date-time"); +formatted_string_impl!(DateTime, "date-time", JsonSchema for DateTime); diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index 10f9f0f9..07fbce53 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -1,52 +1,69 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use serde_json::json; -use std::ops::{Bound, Range, RangeInclusive}; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use core::ops::{Bound, Range, RangeInclusive}; +use serde_json::Value; impl JsonSchema for Option { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - format!("Nullable_{}", T::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Nullable_{}", T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = gen.subschema_for::(); - if gen.settings().option_add_null_type { - schema = match schema { - Schema::Bool(true) => Schema::Bool(true), - Schema::Bool(false) => <()>::json_schema(gen), - Schema::Object(SchemaObject { - instance_type: Some(ref mut instance_type), - .. - }) => { - add_null_type(instance_type); - schema - } - schema => SchemaObject { - // TODO technically the schema already accepts null, so this may be unnecessary - subschemas: Some(Box::new(SubschemaValidation { - any_of: Some(vec![schema, <()>::json_schema(gen)]), - ..Default::default() - })), - ..Default::default() + fn schema_id() -> Cow<'static, str> { + format!("Option<{}>", T::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = generator.subschema_for::(); + + if generator.settings().option_add_null_type { + schema = match schema.try_to_object() { + Ok(mut obj) => { + let instance_type = obj.get_mut("type"); + match instance_type { + Some(Value::Array(array)) => { + let null = Value::from("null"); + if !array.contains(&null) { + array.push(null); + } + obj.into() + } + Some(Value::String(string)) => { + if string != "null" { + *instance_type.unwrap() = Value::Array(vec![ + core::mem::take(string).into(), + "null".into(), + ]); + } + obj.into() + } + _ => json_schema!({ + "anyOf": [ + obj, + <()>::json_schema(generator) + ] + }), + } } - .into(), + Err(true) => true.into(), + Err(false) => <()>::json_schema(generator), } } - if gen.settings().option_nullable { - let mut schema_obj = schema.into_object(); - schema_obj - .extensions - .insert("nullable".to_owned(), json!(true)); - schema = Schema::Object(schema_obj); + + if generator.settings().option_nullable { + schema + .ensure_object() + .insert("nullable".into(), true.into()); }; + schema } - fn _schemars_private_non_optional_json_schema(gen: &mut SchemaGenerator) -> Schema { - T::_schemars_private_non_optional_json_schema(gen) + fn _schemars_private_non_optional_json_schema(generator: &mut SchemaGenerator) -> Schema { + T::_schemars_private_non_optional_json_schema(generator) } fn _schemars_private_is_option() -> bool { @@ -54,160 +71,96 @@ impl JsonSchema for Option { } } -fn add_null_type(instance_type: &mut SingleOrVec) { - match instance_type { - SingleOrVec::Single(ty) if **ty != InstanceType::Null => { - *instance_type = vec![**ty, InstanceType::Null].into() - } - SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => ty.push(InstanceType::Null), - _ => {} - }; -} - impl JsonSchema for Result { - fn schema_name() -> String { - format!("Result_of_{}_or_{}", T::schema_name(), E::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Result_of_{}_or_{}", T::schema_name(), E::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut ok_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = ok_schema.object(); - obj.required.insert("Ok".to_owned()); - obj.properties - .insert("Ok".to_owned(), gen.subschema_for::()); - - let mut err_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = err_schema.object(); - obj.required.insert("Err".to_owned()); - obj.properties - .insert("Err".to_owned(), gen.subschema_for::()); - - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![ok_schema.into(), err_schema.into()]); - schema.into() + fn schema_id() -> Cow<'static, str> { + format!("Result<{}, {}>", T::schema_id(), E::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Ok": generator.subschema_for::() + }, + "required": ["Ok"] + }, + { + "type": "object", + "properties": { + "Err": generator.subschema_for::() + }, + "required": ["Err"] + } + ] + }) } } impl JsonSchema for Bound { - fn schema_name() -> String { - format!("Bound_of_{}", T::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Bound_of_{}", T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut included_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = included_schema.object(); - obj.required.insert("Included".to_owned()); - obj.properties - .insert("Included".to_owned(), gen.subschema_for::()); - - let mut excluded_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = excluded_schema.object(); - obj.required.insert("Excluded".to_owned()); - obj.properties - .insert("Excluded".to_owned(), gen.subschema_for::()); - - let unbounded_schema = SchemaObject { - instance_type: Some(InstanceType::String.into()), - const_value: Some(json!("Unbounded")), - ..Default::default() - }; + fn schema_id() -> Cow<'static, str> { + format!("Bound<{}>", T::schema_id()).into() + } - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![ - included_schema.into(), - excluded_schema.into(), - unbounded_schema.into(), - ]); - schema.into() + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Included": generator.subschema_for::() + }, + "required": ["Included"] + }, + { + "type": "object", + "properties": { + "Excluded": generator.subschema_for::() + }, + "required": ["Excluded"] + }, + { + "type": "string", + "const": "Unbounded" + } + ] + }) } } impl JsonSchema for Range { - fn schema_name() -> String { - format!("Range_of_{}", T::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Range_of_{}", T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("start".to_owned()); - obj.required.insert("end".to_owned()); - obj.properties - .insert("start".to_owned(), gen.subschema_for::()); - obj.properties - .insert("end".to_owned(), gen.subschema_for::()); - schema.into() + fn schema_id() -> Cow<'static, str> { + format!("Range<{}>", T::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let subschema = generator.subschema_for::(); + json_schema!({ + "type": "object", + "properties": { + "start": subschema, + "end": subschema + }, + "required": ["start", "end"] + }) } } forward_impl!(( JsonSchema for RangeInclusive) => Range); -forward_impl!(( JsonSchema for std::marker::PhantomData) => ()); - -forward_impl!((<'a> JsonSchema for std::fmt::Arguments<'a>) => String); - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_option() { - let schema = schema_object_for::>(); - assert_eq!( - schema.instance_type, - Some(vec![InstanceType::Integer, InstanceType::Null].into()) - ); - assert_eq!(schema.extensions.get("nullable"), None); - assert_eq!(schema.subschemas.is_none(), true); - } - - #[test] - fn schema_for_option_with_ref() { - use crate as schemars; - #[derive(JsonSchema)] - struct Foo; - - let schema = schema_object_for::>(); - assert_eq!(schema.instance_type, None); - assert_eq!(schema.extensions.get("nullable"), None); - assert_eq!(schema.subschemas.is_some(), true); - let any_of = schema.subschemas.unwrap().any_of.unwrap(); - assert_eq!(any_of.len(), 2); - assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string())); - assert_eq!(any_of[1], schema_for::<()>()); - } +forward_impl!(( JsonSchema for core::marker::PhantomData) => ()); - #[test] - fn schema_for_result() { - let schema = schema_object_for::>(); - let one_of = schema.subschemas.unwrap().one_of.unwrap(); - assert_eq!(one_of.len(), 2); - - let ok_schema: SchemaObject = one_of[0].clone().into(); - let obj = ok_schema.object.unwrap(); - assert!(obj.required.contains("Ok")); - assert_eq!(obj.properties["Ok"], schema_for::()); - - let err_schema: SchemaObject = one_of[1].clone().into(); - let obj = err_schema.object.unwrap(); - assert!(obj.required.contains("Err")); - assert_eq!(obj.properties["Err"], schema_for::()); - } -} +forward_impl!((<'a> JsonSchema for core::fmt::Arguments<'a>) => String); diff --git a/schemars/src/json_schema_impls/decimal.rs b/schemars/src/json_schema_impls/decimal.rs index d7fc8a1f..afba4abe 100644 --- a/schemars/src/json_schema_impls/decimal.rs +++ b/schemars/src/json_schema_impls/decimal.rs @@ -1,31 +1,27 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::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!(); + always_inline!(); - fn schema_name() -> String { - $name.to_owned() + fn schema_name() -> Cow<'static, str> { + "Decimal".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "pattern": r"^-?[0-9]+(\.[0-9]+)?$", + }) } } }; } -#[cfg(feature = "rust_decimal")] -decimal_impl!(rust_decimal::Decimal); -#[cfg(feature = "bigdecimal")] -decimal_impl!(bigdecimal::BigDecimal); +#[cfg(feature = "rust_decimal1")] +decimal_impl!(rust_decimal1::Decimal); +#[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 deleted file mode 100644 index 1010bc4e..00000000 --- a/schemars/src/json_schema_impls/either.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use either::Either; - -impl JsonSchema for Either { - no_ref_schema!(); - - fn schema_name() -> String { - format!("Either_{}_or_{}", L::schema_name(), R::schema_name()) - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject::default(); - schema.subschemas().any_of = Some(vec![gen.subschema_for::(), gen.subschema_for::()]); - schema.into() - } -} diff --git a/schemars/src/json_schema_impls/either1.rs b/schemars/src/json_schema_impls/either1.rs new file mode 100644 index 00000000..0e9a1299 --- /dev/null +++ b/schemars/src/json_schema_impls/either1.rs @@ -0,0 +1,23 @@ +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use either1::Either; + +impl JsonSchema for Either { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + format!("Either_{}_or_{}", L::schema_name(), R::schema_name()).into() + } + + fn schema_id() -> Cow<'static, str> { + format!("either::Either<{}, {}>", L::schema_id(), R::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "anyOf": [generator.subschema_for::(), generator.subschema_for::()], + }) + } +} diff --git a/schemars/src/json_schema_impls/enumset.rs b/schemars/src/json_schema_impls/enumset.rs deleted file mode 100644 index 22a3ebcf..00000000 --- a/schemars/src/json_schema_impls/enumset.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use enumset::{EnumSet, EnumSetType}; - -forward_impl!(( JsonSchema for EnumSet where T: EnumSetType + JsonSchema) => std::collections::BTreeSet); diff --git a/schemars/src/json_schema_impls/ffi.rs b/schemars/src/json_schema_impls/ffi.rs index 5ce10d75..cf2efe5a 100644 --- a/schemars/src/json_schema_impls/ffi.rs +++ b/schemars/src/json_schema_impls/ffi.rs @@ -1,35 +1,37 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; impl JsonSchema for OsString { - fn schema_name() -> String { - "OsString".to_owned() + fn schema_name() -> Cow<'static, str> { + "OsString".into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut unix_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = unix_schema.object(); - obj.required.insert("Unix".to_owned()); - obj.properties - .insert("Unix".to_owned(), >::json_schema(gen)); - - let mut win_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = win_schema.object(); - obj.required.insert("Windows".to_owned()); - obj.properties - .insert("Windows".to_owned(), >::json_schema(gen)); + fn schema_id() -> Cow<'static, str> { + "std::ffi::OsString".into() + } - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![unix_schema.into(), win_schema.into()]); - schema.into() + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Unix": >::json_schema(generator) + }, + "required": ["Unix"] + }, + { + "type": "object", + "properties": { + "Windows": >::json_schema(generator) + }, + "required": ["Windows"] + }, + ] + }) } } diff --git a/schemars/src/json_schema_impls/indexmap.rs b/schemars/src/json_schema_impls/indexmap.rs deleted file mode 100644 index 3bd3e545..00000000 --- a/schemars/src/json_schema_impls/indexmap.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use indexmap::{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/indexmap2.rs b/schemars/src/json_schema_impls/indexmap2.rs new file mode 100644 index 00000000..355864eb --- /dev/null +++ b/schemars/src/json_schema_impls/indexmap2.rs @@ -0,0 +1,6 @@ +use crate::JsonSchema; +use alloc::collections::{BTreeMap, BTreeSet}; +use indexmap2::{IndexMap, IndexSet}; + +forward_impl!(( JsonSchema for IndexMap) => BTreeMap); +forward_impl!(( JsonSchema for IndexSet) => BTreeSet); diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs index 356464fd..86d43c8b 100644 --- a/schemars/src/json_schema_impls/maps.rs +++ b/schemars/src/json_schema_impls/maps.rs @@ -1,6 +1,6 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema, SchemaGenerator}; +use alloc::borrow::Cow; macro_rules! map_impl { ($($desc:tt)+) => { @@ -8,27 +8,27 @@ macro_rules! map_impl { where V: JsonSchema, { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - format!("Map_of_{}", V::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Map_of_{}", V::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let subschema = gen.subschema_for::(); - SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(subschema)), - ..Default::default() - })), - ..Default::default() - } - .into() + fn schema_id() -> Cow<'static, str> { + format!("Map<{}>", V::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "object", + "additionalProperties": generator.subschema_for::(), + }) } } }; } -map_impl!( JsonSchema for std::collections::BTreeMap); +map_impl!( JsonSchema for alloc::collections::BTreeMap); + +#[cfg(feature = "std")] map_impl!( JsonSchema for std::collections::HashMap); diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs index 5fddb988..89b7fe37 100644 --- a/schemars/src/json_schema_impls/mod.rs +++ b/schemars/src/json_schema_impls/mod.rs @@ -1,7 +1,7 @@ -macro_rules! no_ref_schema { +macro_rules! always_inline { () => { - fn is_referenceable() -> bool { - false + fn always_inline_schema() -> bool { + true } }; } @@ -9,20 +9,24 @@ macro_rules! no_ref_schema { macro_rules! forward_impl { (($($impl:tt)+) => $target:ty) => { impl $($impl)+ { - fn is_referenceable() -> bool { - <$target>::is_referenceable() + fn always_inline_schema() -> bool { + <$target>::always_inline_schema() } - fn schema_name() -> String { + fn schema_name() -> alloc::borrow::Cow<'static, str> { <$target>::schema_name() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - <$target>::json_schema(gen) + fn schema_id() -> alloc::borrow::Cow<'static, str> { + <$target>::schema_id() } - fn _schemars_private_non_optional_json_schema(gen: &mut SchemaGenerator) -> Schema { - <$target>::_schemars_private_non_optional_json_schema(gen) + fn json_schema(generator: &mut $crate::SchemaGenerator) -> $crate::Schema { + <$target>::json_schema(generator) + } + + fn _schemars_private_non_optional_json_schema(generator: &mut $crate::SchemaGenerator) -> $crate::Schema { + <$target>::_schemars_private_non_optional_json_schema(generator) } fn _schemars_private_is_option() -> bool { @@ -31,49 +35,66 @@ macro_rules! forward_impl { } }; ($ty:ty => $target:ty) => { - forward_impl!((JsonSchema for $ty) => $target); + forward_impl!(($crate::JsonSchema for $ty) => $target); }; } mod array; -#[cfg(feature = "arrayvec05")] -mod arrayvec05; -#[cfg(feature = "arrayvec07")] -mod arrayvec07; -#[cfg(std_atomic)] -mod atomic; -#[cfg(feature = "bytes")] -mod bytes; -#[cfg(feature = "chrono")] -mod chrono; mod core; -#[cfg(any(feature = "rust_decimal", feature = "bigdecimal"))] -mod decimal; -#[cfg(feature = "either")] -mod either; -#[cfg(feature = "enumset")] -mod enumset; -mod ffi; -#[cfg(feature = "indexmap")] -mod indexmap; mod maps; mod nonzero_signed; mod nonzero_unsigned; mod primitives; mod sequences; mod serdejson; -#[cfg(feature = "smallvec")] -mod smallvec; -#[cfg(feature = "smol_str")] -mod smol_str; -mod time; +mod std_time; +mod tuple; +mod wrapper; + +#[cfg(target_has_atomic)] +mod atomic; + +#[cfg(feature = "std")] +mod ffi; + +#[cfg(feature = "arrayvec07")] +mod arrayvec07; + +#[cfg(feature = "bytes1")] +mod bytes1 { + forward_impl!(bytes1::Bytes => alloc::vec::Vec); + forward_impl!(bytes1::BytesMut => alloc::vec::Vec); +} + +#[cfg(feature = "chrono04")] +mod chrono04; + +#[cfg(any(feature = "rust_decimal1", feature = "bigdecimal04"))] +mod decimal; + +#[cfg(feature = "either1")] +mod either1; + +#[cfg(feature = "enumset1")] +forward_impl!(( crate::JsonSchema for enumset1::EnumSet) => alloc::collections::BTreeSet); + +#[cfg(feature = "indexmap2")] +mod indexmap2; + +#[cfg(feature = "semver1")] +mod semver1; + #[cfg(feature = "triomphe")] mod triomphe; -mod tuple; -#[cfg(feature = "url")] -mod url; -#[cfg(feature = "uuid08")] -mod uuid08; + +#[cfg(feature = "smallvec1")] +forward_impl!(( crate::JsonSchema for smallvec1::SmallVec where A::Item: crate::JsonSchema) => alloc::vec::Vec); + +#[cfg(feature = "smol_str02")] +forward_impl!(smol_str02::SmolStr => alloc::string::String); + +#[cfg(feature = "url2")] +mod url2; + #[cfg(feature = "uuid1")] mod uuid1; -mod wrapper; diff --git a/schemars/src/json_schema_impls/nonzero_signed.rs b/schemars/src/json_schema_impls/nonzero_signed.rs index 00bb63ff..c5b3e84c 100644 --- a/schemars/src/json_schema_impls/nonzero_signed.rs +++ b/schemars/src/json_schema_impls/nonzero_signed.rs @@ -1,26 +1,29 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use std::num::*; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{JsonSchema, Schema}; +use alloc::borrow::Cow; +use core::num::*; macro_rules! nonzero_unsigned_impl { ($type:ty => $primitive:ty) => { impl JsonSchema for $type { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - stringify!($type).to_owned() + fn schema_name() -> Cow<'static, str> { + stringify!($type).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let zero_schema: Schema = SchemaObject { - const_value: Some(0.into()), - ..Default::default() - } - .into(); - let mut schema: SchemaObject = <$primitive>::json_schema(gen).into(); - schema.subschemas().not = Some(Box::from(zero_schema)); - schema.into() + fn schema_id() -> Cow<'static, str> { + stringify!(std::num::$type).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = <$primitive>::json_schema(generator); + let object = schema.ensure_object(); + object.insert("not".to_owned(), serde_json::json!({ + "const": 0 + })); + schema } } }; diff --git a/schemars/src/json_schema_impls/nonzero_unsigned.rs b/schemars/src/json_schema_impls/nonzero_unsigned.rs index 284ac8a6..c6711abc 100644 --- a/schemars/src/json_schema_impls/nonzero_unsigned.rs +++ b/schemars/src/json_schema_impls/nonzero_unsigned.rs @@ -1,21 +1,28 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; use crate::JsonSchema; -use std::num::*; +use crate::Schema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use alloc::borrow::Cow; +use core::num::*; macro_rules! nonzero_unsigned_impl { ($type:ty => $primitive:ty) => { impl JsonSchema for $type { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - stringify!($type).to_owned() + fn schema_name() -> Cow<'static, str> { + stringify!($type).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = <$primitive>::json_schema(gen).into(); - schema.number().minimum = Some(1.0); - schema.into() + fn schema_id() -> Cow<'static, str> { + stringify!(std::num::$type).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema = <$primitive>::json_schema(generator); + let object = schema.ensure_object(); + object.insert("minimum".to_owned(), 1.into()); + schema } } }; @@ -27,18 +34,3 @@ nonzero_unsigned_impl!(NonZeroU32 => u32); nonzero_unsigned_impl!(NonZeroU64 => u64); nonzero_unsigned_impl!(NonZeroU128 => u128); nonzero_unsigned_impl!(NonZeroUsize => usize); - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::schema_object_for; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_nonzero_u32() { - let schema = schema_object_for::(); - assert_eq!(schema.number.unwrap().minimum, Some(1.0)); - assert_eq!(schema.instance_type, Some(InstanceType::Integer.into())); - assert_eq!(schema.format, Some("uint32".to_owned())); - } -} diff --git a/schemars/src/json_schema_impls/primitives.rs b/schemars/src/json_schema_impls/primitives.rs index acdcd7e9..e59bc5d1 100644 --- a/schemars/src/json_schema_impls/primitives.rs +++ b/schemars/src/json_schema_impls/primitives.rs @@ -1,106 +1,116 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; -use std::path::{Path, PathBuf}; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; macro_rules! simple_impl { - ($type:ty => $instance_type:ident) => { - simple_impl!($type => $instance_type, stringify!($instance_type), None); - }; - ($type:ty => $instance_type:ident, $format:literal) => { - simple_impl!($type => $instance_type, $format, Some($format.to_owned())); + ($type:ty => $instance_type:literal) => { + impl JsonSchema for $type { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + $instance_type.into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": $instance_type + }) + } + } }; - ($type:ty => $instance_type:ident, $name:expr, $format:expr) => { + ($type:ty => $instance_type:literal, $format:literal) => { impl JsonSchema for $type { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - $name.to_owned() + fn schema_name() -> Cow<'static, str> { + $format.into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - format: $format, - ..Default::default() - } - .into() + json_schema!({ + "type": $instance_type, + "format": $format + }) } } }; } -simple_impl!(str => String); -simple_impl!(String => String); -simple_impl!(bool => Boolean); -simple_impl!(f32 => Number, "float"); -simple_impl!(f64 => Number, "double"); -simple_impl!(i8 => Integer, "int8"); -simple_impl!(i16 => Integer, "int16"); -simple_impl!(i32 => Integer, "int32"); -simple_impl!(i64 => Integer, "int64"); -simple_impl!(i128 => Integer, "int128"); -simple_impl!(isize => Integer, "int"); -simple_impl!(() => Null); - -simple_impl!(Path => String); -simple_impl!(PathBuf => String); - -simple_impl!(Ipv4Addr => String, "ipv4"); -simple_impl!(Ipv6Addr => String, "ipv6"); -simple_impl!(IpAddr => String, "ip"); - -simple_impl!(SocketAddr => String); -simple_impl!(SocketAddrV4 => String); -simple_impl!(SocketAddrV6 => String); +simple_impl!(str => "string"); +simple_impl!(String => "string"); +simple_impl!(bool => "boolean"); +simple_impl!(f32 => "number", "float"); +simple_impl!(f64 => "number", "double"); +simple_impl!(i8 => "integer", "int8"); +simple_impl!(i16 => "integer", "int16"); +simple_impl!(i32 => "integer", "int32"); +simple_impl!(i64 => "integer", "int64"); +simple_impl!(i128 => "integer", "int128"); +simple_impl!(isize => "integer", "int"); +simple_impl!(() => "null"); + +#[cfg(feature = "std")] +mod std_types { + use super::*; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + use std::path::{Path, PathBuf}; + + simple_impl!(Path => "string"); + simple_impl!(PathBuf => "string"); + + simple_impl!(Ipv4Addr => "string", "ipv4"); + simple_impl!(Ipv6Addr => "string", "ipv6"); + simple_impl!(IpAddr => "string", "ip"); + + simple_impl!(SocketAddr => "string"); + simple_impl!(SocketAddrV4 => "string"); + simple_impl!(SocketAddrV6 => "string"); +} macro_rules! unsigned_impl { - ($type:ty => $instance_type:ident, $format:expr) => { + ($type:ty => $instance_type:literal, $format:literal) => { impl JsonSchema for $type { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - $format.to_owned() + fn schema_name() -> Cow<'static, str> { + $format.into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - format: Some($format.to_owned()), - ..Default::default() - }; - schema.number().minimum = Some(0.0); - schema.into() + json_schema!({ + "type": $instance_type, + "format": $format, + "minimum": 0 + }) } } }; } -unsigned_impl!(u8 => Integer, "uint8"); -unsigned_impl!(u16 => Integer, "uint16"); -unsigned_impl!(u32 => Integer, "uint32"); -unsigned_impl!(u64 => Integer, "uint64"); -unsigned_impl!(u128 => Integer, "uint128"); -unsigned_impl!(usize => Integer, "uint"); +unsigned_impl!(u8 => "integer", "uint8"); +unsigned_impl!(u16 => "integer", "uint16"); +unsigned_impl!(u32 => "integer", "uint32"); +unsigned_impl!(u64 => "integer", "uint64"); +unsigned_impl!(u128 => "integer", "uint128"); +unsigned_impl!(usize => "integer", "uint"); impl JsonSchema for char { - no_ref_schema!(); + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + "Character".into() + } - fn schema_name() -> String { - "Character".to_owned() + fn schema_id() -> Cow<'static, str> { + "char".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - min_length: Some(1), - max_length: Some(1), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "minLength": 1, + "maxLength": 1, + }) } } diff --git a/schemars/src/json_schema_impls/semver1.rs b/schemars/src/json_schema_impls/semver1.rs new file mode 100644 index 00000000..1869fa92 --- /dev/null +++ b/schemars/src/json_schema_impls/semver1.rs @@ -0,0 +1,24 @@ +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use semver1::Version; + +impl JsonSchema for Version { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + "Version".into() + } + + fn schema_id() -> Cow<'static, str> { + "semver::Version".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + "pattern": 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-]+)*))?$" + }) + } +} diff --git a/schemars/src/json_schema_impls/sequences.rs b/schemars/src/json_schema_impls/sequences.rs index 58bb8e78..4af62a2a 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 crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; macro_rules! seq_impl { ($($desc:tt)+) => { @@ -8,22 +9,21 @@ macro_rules! seq_impl { where T: JsonSchema, { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - format!("Array_of_{}", T::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Array_of_{}", T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - ..Default::default() - })), - ..Default::default() - } - .into() + fn schema_id() -> Cow<'static, str> { + format!("[{}]", T::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "items": generator.subschema_for::(), + }) } } }; @@ -35,33 +35,34 @@ macro_rules! set_impl { where T: JsonSchema, { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - format!("Set_of_{}", T::schema_name()) + fn schema_name() -> Cow<'static, str> { + format!("Set_of_{}", T::schema_name()).into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - unique_items: Some(true), - items: Some(gen.subschema_for::().into()), - ..Default::default() - })), - ..Default::default() - } - .into() + fn schema_id() -> Cow<'static, str> { + format!("Set<{}>", T::schema_id()).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "uniqueItems": true, + "items": generator.subschema_for::(), + }) } } }; } -seq_impl!( JsonSchema for std::collections::BinaryHeap); -seq_impl!( JsonSchema for std::collections::LinkedList); +seq_impl!( JsonSchema for alloc::collections::BinaryHeap); +seq_impl!( JsonSchema for alloc::collections::LinkedList); seq_impl!( JsonSchema for [T]); -seq_impl!( JsonSchema for Vec); -seq_impl!( JsonSchema for std::collections::VecDeque); +seq_impl!( JsonSchema for alloc::vec::Vec); +seq_impl!( JsonSchema for alloc::collections::VecDeque); + +set_impl!( JsonSchema for alloc::collections::BTreeSet); -set_impl!( JsonSchema for std::collections::BTreeSet); +#[cfg(feature = "std")] set_impl!( JsonSchema for std::collections::HashSet); diff --git a/schemars/src/json_schema_impls/serdejson.rs b/schemars/src/json_schema_impls/serdejson.rs index d0cceff6..2b6aa7d0 100644 --- a/schemars/src/json_schema_impls/serdejson.rs +++ b/schemars/src/json_schema_impls/serdejson.rs @@ -1,35 +1,37 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use alloc::collections::BTreeMap; use serde_json::{Map, Number, Value}; -use std::collections::BTreeMap; impl JsonSchema for Value { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - "AnyValue".to_owned() + fn schema_name() -> Cow<'static, str> { + "AnyValue".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Bool(true) + true.into() } } forward_impl!(Map => BTreeMap); impl JsonSchema for Number { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - "Number".to_owned() + fn schema_name() -> Cow<'static, str> { + "Number".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Number.into()), - ..Default::default() - } - .into() + json_schema!({ + "type": "number" + }) } } + +#[cfg(feature = "raw_value")] +forward_impl!(serde_json::value::RawValue => Value); diff --git a/schemars/src/json_schema_impls/smallvec.rs b/schemars/src/json_schema_impls/smallvec.rs deleted file mode 100644 index f7a75e37..00000000 --- a/schemars/src/json_schema_impls/smallvec.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use smallvec::{Array, SmallVec}; - -forward_impl!(( JsonSchema for SmallVec where A::Item: JsonSchema) => Vec); diff --git a/schemars/src/json_schema_impls/smol_str.rs b/schemars/src/json_schema_impls/smol_str.rs deleted file mode 100644 index cbca4a15..00000000 --- a/schemars/src/json_schema_impls/smol_str.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use smol_str::SmolStr; - -forward_impl!(SmolStr => String); diff --git a/schemars/src/json_schema_impls/std_time.rs b/schemars/src/json_schema_impls/std_time.rs new file mode 100644 index 00000000..22b3fd9f --- /dev/null +++ b/schemars/src/json_schema_impls/std_time.rs @@ -0,0 +1,46 @@ +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; + +impl JsonSchema for core::time::Duration { + fn schema_name() -> Cow<'static, str> { + "Duration".into() + } + + fn schema_id() -> Cow<'static, str> { + "std::time::Duration".into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "object", + "required": ["secs", "nanos"], + "properties": { + "secs": u64::json_schema(generator), + "nanos": u32::json_schema(generator), + } + }) + } +} + +#[cfg(feature = "std")] +impl JsonSchema for std::time::SystemTime { + fn schema_name() -> Cow<'static, str> { + "SystemTime".into() + } + + fn schema_id() -> Cow<'static, str> { + "std::time::SystemTime".into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "object", + "required": ["secs_since_epoch", "nanos_since_epoch"], + "properties": { + "secs_since_epoch": u64::json_schema(generator), + "nanos_since_epoch": u32::json_schema(generator), + } + }) + } +} diff --git a/schemars/src/json_schema_impls/time.rs b/schemars/src/json_schema_impls/time.rs deleted file mode 100644 index f4362eef..00000000 --- a/schemars/src/json_schema_impls/time.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use std::time::{Duration, SystemTime}; - -impl JsonSchema for Duration { - fn schema_name() -> String { - "Duration".to_owned() - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("secs".to_owned()); - obj.required.insert("nanos".to_owned()); - obj.properties - .insert("secs".to_owned(), ::json_schema(gen)); - obj.properties - .insert("nanos".to_owned(), ::json_schema(gen)); - schema.into() - } -} - -impl JsonSchema for SystemTime { - fn schema_name() -> String { - "SystemTime".to_owned() - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("secs_since_epoch".to_owned()); - obj.required.insert("nanos_since_epoch".to_owned()); - obj.properties - .insert("secs_since_epoch".to_owned(), ::json_schema(gen)); - obj.properties - .insert("nanos_since_epoch".to_owned(), ::json_schema(gen)); - schema.into() - } -} diff --git a/schemars/src/json_schema_impls/triomphe.rs b/schemars/src/json_schema_impls/triomphe.rs index 9874b3f4..956da321 100644 --- a/schemars/src/json_schema_impls/triomphe.rs +++ b/schemars/src/json_schema_impls/triomphe.rs @@ -1,5 +1,3 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; use crate::JsonSchema; forward_impl!(( JsonSchema for triomphe::Arc where T: JsonSchema) => T); diff --git a/schemars/src/json_schema_impls/tuple.rs b/schemars/src/json_schema_impls/tuple.rs index 383f839c..88cc0fff 100644 --- a/schemars/src/json_schema_impls/tuple.rs +++ b/schemars/src/json_schema_impls/tuple.rs @@ -1,34 +1,37 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; macro_rules! tuple_impls { ($($len:expr => ($($name:ident)+))+) => { $( impl<$($name: JsonSchema),+> JsonSchema for ($($name,)+) { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { + fn schema_name() -> Cow<'static, str> { let mut name = "Tuple_of_".to_owned(); name.push_str(&[$($name::schema_name()),+].join("_and_")); - name + name.into() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let items = vec![ - $(gen.subschema_for::<$name>()),+ - ]; - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(items.into()), - max_items: Some($len), - min_items: Some($len), - ..Default::default() - })), - ..Default::default() - } - .into() + fn schema_id() -> Cow<'static, str> { + let mut id = "(".to_owned(); + id.push_str(&[$($name::schema_id()),+].join(",")); + id.push(')'); + + id.into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "array", + "prefixItems": [ + $(generator.subschema_for::<$name>()),+ + ], + "minItems": $len, + "maxItems": $len, + }) } } )+ @@ -53,29 +56,3 @@ tuple_impls! { 15 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14) 16 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_map_any_value() { - let schema = schema_object_for::<(i32, bool)>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!( - array_validation.items, - Some(SingleOrVec::Vec(vec![ - schema_for::(), - schema_for::() - ])) - ); - assert_eq!(array_validation.max_items, Some(2)); - assert_eq!(array_validation.min_items, Some(2)); - } -} diff --git a/schemars/src/json_schema_impls/url.rs b/schemars/src/json_schema_impls/url.rs deleted file mode 100644 index ffc5426f..00000000 --- a/schemars/src/json_schema_impls/url.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use url::Url; - -impl JsonSchema for Url { - no_ref_schema!(); - - fn schema_name() -> String { - "Url".to_owned() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uri".to_owned()), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/url2.rs b/schemars/src/json_schema_impls/url2.rs new file mode 100644 index 00000000..6fcaceb9 --- /dev/null +++ b/schemars/src/json_schema_impls/url2.rs @@ -0,0 +1,23 @@ +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; +use url2::Url; + +impl JsonSchema for Url { + always_inline!(); + + fn schema_name() -> Cow<'static, str> { + "Url".into() + } + + fn schema_id() -> Cow<'static, str> { + "url::Url".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + "format": "uri", + }) + } +} diff --git a/schemars/src/json_schema_impls/uuid08.rs b/schemars/src/json_schema_impls/uuid08.rs deleted file mode 100644 index 9e6dc587..00000000 --- a/schemars/src/json_schema_impls/uuid08.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use uuid08::Uuid; - -impl JsonSchema for Uuid { - no_ref_schema!(); - - fn schema_name() -> String { - "Uuid".to_string() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uuid".to_string()), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/uuid1.rs b/schemars/src/json_schema_impls/uuid1.rs index 85d67899..677c279d 100644 --- a/schemars/src/json_schema_impls/uuid1.rs +++ b/schemars/src/json_schema_impls/uuid1.rs @@ -1,21 +1,23 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use alloc::borrow::Cow; use uuid1::Uuid; impl JsonSchema for Uuid { - no_ref_schema!(); + always_inline!(); - fn schema_name() -> String { - "Uuid".to_string() + fn schema_name() -> Cow<'static, str> { + "Uuid".into() + } + + fn schema_id() -> Cow<'static, str> { + "uuid::Uuid".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uuid".to_string()), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "format": "uuid", + }) } } diff --git a/schemars/src/json_schema_impls/wrapper.rs b/schemars/src/json_schema_impls/wrapper.rs index 243a06b5..ea095049 100644 --- a/schemars/src/json_schema_impls/wrapper.rs +++ b/schemars/src/json_schema_impls/wrapper.rs @@ -1,6 +1,5 @@ -use crate::gen::SchemaGenerator; -use crate::schema::Schema; use crate::JsonSchema; +use crate::_alloc_prelude::*; macro_rules! wrapper_impl { ($($desc:tt)+) => { @@ -11,14 +10,16 @@ macro_rules! wrapper_impl { wrapper_impl!(<'a, T: ?Sized> JsonSchema for &'a T); wrapper_impl!(<'a, T: ?Sized> JsonSchema for &'a mut T); wrapper_impl!( JsonSchema for Box); -wrapper_impl!( JsonSchema for std::rc::Rc); -wrapper_impl!( JsonSchema for std::rc::Weak); -wrapper_impl!( JsonSchema for std::sync::Arc); -wrapper_impl!( JsonSchema for std::sync::Weak); +wrapper_impl!( JsonSchema for alloc::rc::Rc); +wrapper_impl!( JsonSchema for alloc::rc::Weak); +wrapper_impl!( JsonSchema for alloc::sync::Arc); +wrapper_impl!( JsonSchema for alloc::sync::Weak); +#[cfg(feature = "std")] wrapper_impl!( JsonSchema for std::sync::Mutex); +#[cfg(feature = "std")] wrapper_impl!( JsonSchema for std::sync::RwLock); -wrapper_impl!( JsonSchema for std::cell::Cell); -wrapper_impl!( JsonSchema for std::cell::RefCell); -wrapper_impl!(<'a, T: ?Sized + ToOwned> JsonSchema for std::borrow::Cow<'a, T>); -wrapper_impl!( JsonSchema for std::num::Wrapping); -wrapper_impl!( JsonSchema for std::cmp::Reverse); +wrapper_impl!( JsonSchema for core::cell::Cell); +wrapper_impl!( JsonSchema for core::cell::RefCell); +wrapper_impl!(<'a, T: ?Sized + ToOwned> JsonSchema for alloc::borrow::Cow<'a, T>); +wrapper_impl!( JsonSchema for core::num::Wrapping); +wrapper_impl!( JsonSchema for core::cmp::Reverse); diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index 796ec272..9bc458e1 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -1,314 +1,21 @@ -#![forbid(unsafe_code)] -/*! -Generate JSON Schema documents from Rust code +#![deny(unsafe_code, clippy::cargo, clippy::pedantic)] +#![allow( + clippy::must_use_candidate, + clippy::return_self_not_must_use, + clippy::wildcard_imports, + clippy::single_match_else, + clippy::missing_errors_doc, + clippy::module_name_repetitions +)] +#![doc = include_str!("../README.md")] +#![no_std] + +extern crate alloc; +#[cfg(feature = "std")] +extern crate std; -## 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) - -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"] } -``` -*/ - -/// The map type used by schemars types. -/// -/// Currently a `BTreeMap` or `IndexMap` can be used, but this may change to a different implementation -/// with a similar interface in a future version of schemars. -/// The `IndexMap` will be used when the `preserve_order` feature flag is set. -#[cfg(not(feature = "preserve_order"))] -pub type Map = std::collections::BTreeMap; -#[cfg(feature = "preserve_order")] -pub type Map = indexmap::IndexMap; -/// The set type used by schemars types. -/// -/// Currently a `BTreeSet`, but this may change to a different implementation -/// with a similar interface in a future version of schemars. -pub type Set = std::collections::BTreeSet; - -/// A view into a single entry in a map, which may either be vacant or occupied. -// -/// This is constructed from the `entry` method on `BTreeMap` or `IndexMap`, -/// depending on whether the `preserve_order` feature flag is set. -#[cfg(not(feature = "preserve_order"))] -pub type MapEntry<'a, K, V> = std::collections::btree_map::Entry<'a, K, V>; -#[cfg(feature = "preserve_order")] -pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>; - -mod flatten; mod json_schema_impls; +mod schema; mod ser; #[macro_use] mod macros; @@ -317,28 +24,50 @@ mod macros; /// outside of `schemars`, and should not be considered part of the public API. #[doc(hidden)] pub mod _private; -pub mod gen; -pub mod schema; -pub mod visit; +pub mod generate; +pub mod transform; #[cfg(feature = "schemars_derive")] extern crate schemars_derive; +use alloc::borrow::Cow; + #[cfg(feature = "schemars_derive")] pub use schemars_derive::*; -// Export serde_json so schemars_derive can use it -#[doc(hidden)] -pub use serde_json as _serde_json; +#[doc(inline)] +pub use generate::SchemaGenerator; +pub use schema::Schema; + +mod _alloc_prelude { + pub use alloc::borrow::ToOwned; + pub use alloc::boxed::Box; + pub use alloc::format; + pub use alloc::string::{String, ToString}; + pub use alloc::vec; + pub use alloc::vec::Vec; +} -use schema::Schema; +#[deprecated = "Only included for backward-compatibility - use the `schemars::generate` module instead."] +#[doc(hidden)] +pub mod r#gen { + #[deprecated = "Only included for backward-compatibility - use `schemars::SchemaGenerator` or `schemars::generate::SchemaGenerator` instead."] + pub type SchemaGenerator = crate::generate::SchemaGenerator; + #[deprecated = "Only included for backward-compatibility - use `schemars::generate::SchemaSettings` instead."] + pub type SchemaSettings = crate::generate::SchemaSettings; + #[deprecated = "Only included for backward-compatibility - use `schemars::generate::GenTransform` instead."] + pub use crate::generate::GenTransform; +} /// A type which can be described as a JSON Schema document. /// /// This is implemented for many Rust primitive and standard library types. /// -/// This can also be automatically derived on most custom types with `#[derive(JsonSchema)]`. +/// This can also be automatically derived on most custom types with `#[derive(JsonSchema)]` by +/// enabling the `derive` feature flag (which is enabled by default). +/// For more info on deriving `JsonSchema`, see [the derive macro documentation](derive@JsonSchema). /// -/// # Example +/// # Examples +/// Deriving an implementation: /// ``` /// use schemars::{schema_for, JsonSchema}; /// @@ -349,34 +78,112 @@ 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::{SchemaGenerator, Schema, JsonSchema, json_schema}; +/// use std::borrow::Cow; +/// +/// struct NonGenericType; +/// +/// impl JsonSchema for NonGenericType { +/// fn schema_name() -> Cow<'static, str> { +/// // Exclude the module path to make the name in generated schemas clearer. +/// "NonGenericType".into() +/// } +/// +/// fn schema_id() -> Cow<'static, str> { +/// // Include the module, in case a type with the same name is in another module/crate +/// concat!(module_path!(), "::NonGenericType").into() +/// } +/// +/// fn json_schema(_gen: &mut SchemaGenerator) -> Schema { +/// json_schema!({ +/// "foo": "bar" +/// }) +/// } +/// } +/// +/// 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::{SchemaGenerator, Schema, JsonSchema, json_schema}; +/// use std::{borrow::Cow, marker::PhantomData}; +/// +/// struct GenericType(PhantomData); +/// +/// impl JsonSchema for GenericType { +/// fn schema_name() -> Cow<'static, str> { +/// format!("GenericType_{}", T::schema_name()).into() +/// } +/// +/// fn schema_id() -> Cow<'static, str> { +/// format!( +/// "{}::GenericType<{}>", +/// module_path!(), +/// T::schema_id() +/// ).into() +/// } +/// +/// fn json_schema(_gen: &mut SchemaGenerator) -> Schema { +/// json_schema!({ +/// "foo": "bar" +/// }) +/// } +/// } +/// +/// 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. + /// Whether JSON Schemas generated for this type should be included directly in parent schemas, + /// rather than being re-used where possible using the `$ref` keyword. /// - /// For trivial types (such as primitives), this should return `false`. For more complex types, it should return `true`. - /// For recursive types, this **must** return `true` to prevent infinite cycles when generating schemas. + /// For trivial types (such as primitives), this should return `true`. For more complex types, + /// it should return `false`. For recursive types, this **must** return `false` to prevent + /// infinite cycles when generating schemas. /// - /// By default, this returns `true`. - fn is_referenceable() -> bool { - true + /// By default, this returns `false`. + fn always_inline_schema() -> bool { + false } /// The name of the generated JSON Schema. /// - /// This is used as the title for root schemas, and the key within the root's `definitions` property for subschemas. - fn schema_name() -> String; + /// This is used as the title for root schemas, and the key within the root's `definitions` + /// property for subschemas. + fn schema_name() -> Cow<'static, str>; + + /// 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()`](JsonSchema::schema_name). + fn schema_id() -> Cow<'static, str> { + 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 - /// add them to the [`SchemaGenerator`](gen::SchemaGenerator)'s schema definitions. + /// If the returned schema depends on any [non-inlined](JsonSchema::always_inline_schema) + /// schemas, then this method will add them to the [`SchemaGenerator`]'s schema definitions. /// /// This should not return a `$ref` schema. - fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema; + fn json_schema(generator: &mut SchemaGenerator) -> Schema; // TODO document and bring into public API? #[doc(hidden)] - fn _schemars_private_non_optional_json_schema(gen: &mut gen::SchemaGenerator) -> Schema { - Self::json_schema(gen) + fn _schemars_private_non_optional_json_schema(generator: &mut SchemaGenerator) -> Schema { + Self::json_schema(generator) } // TODO document and bring into public API? @@ -385,24 +192,3 @@ pub trait JsonSchema { false } } - -#[cfg(test)] -pub mod tests { - use super::*; - - pub fn schema_object_for() -> schema::SchemaObject { - schema_object(schema_for::()) - } - - pub fn schema_for() -> schema::Schema { - let mut gen = gen::SchemaGenerator::default(); - T::json_schema(&mut gen) - } - - pub fn schema_object(schema: schema::Schema) -> schema::SchemaObject { - match schema { - schema::Schema::Object(o) => o, - s => panic!("Schema was not an object: {:?}", s), - } - } -} diff --git a/schemars/src/macros.rs b/schemars/src/macros.rs index 18a6810c..165ed863 100644 --- a/schemars/src/macros.rs +++ b/schemars/src/macros.rs @@ -1,4 +1,5 @@ -/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given type using default settings. +/// Generates a [`Schema`](crate::Schema) for the given type using default settings. +/// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. /// /// The type must implement [`JsonSchema`](crate::JsonSchema). /// @@ -17,11 +18,12 @@ #[macro_export] macro_rules! schema_for { ($type:ty) => { - $crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>() + $crate::SchemaGenerator::default().into_root_schema_for::<$type>() }; } -/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given type using default settings. +/// Generates a [`Schema`](crate::Schema) for the given type using default settings. +/// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. /// /// The type must implement [`JsonSchema`](crate::JsonSchema). /// @@ -40,22 +42,24 @@ macro_rules! schema_for { #[macro_export] macro_rules! schema_for { ($type:ty) => { - $crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>() + $crate::SchemaGenerator::default().into_root_schema_for::<$type>() }; ($_:expr) => { compile_error!("This argument to `schema_for!` is not a type - did you mean to use `schema_for_value!` instead?") }; } -/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given example value using default settings. +/// Generates a [`Schema`](crate::Schema) for the given example value using default settings. +/// The default settings currently conform to [JSON Schema 2020-12](https://json-schema.org/specification-links#2020-12), but this is liable to change in a future version of Schemars if support for other JSON Schema versions is added. /// -/// The value must implement [`Serialize`](serde::Serialize). If the value also implements [`JsonSchema`](crate::JsonSchema), -/// then prefer using the [`schema_for!`](schema_for) macro which will generally produce a more precise schema, -/// particularly when the value contains any enums. +/// The value must implement [`Serialize`](serde::Serialize). If the value also implements +/// [`JsonSchema`](crate::JsonSchema), then prefer using the [`schema_for!(Type)`](schema_for) macro +/// which will generally produce a more precise schema, particularly when the value contains any +/// enums. /// /// If the `Serialize` implementation of the value decides to fail, this macro will panic. -/// For a non-panicking alternative, create a [`SchemaGenerator`](crate::gen::SchemaGenerator) and use -/// its [`into_root_schema_for_value`](crate::gen::SchemaGenerator::into_root_schema_for_value) method. +/// For a non-panicking alternative, create a [`SchemaGenerator`](crate::SchemaGenerator) and use +/// its [`into_root_schema_for_value`](crate::SchemaGenerator::into_root_schema_for_value) method. /// /// # Example /// ``` @@ -71,8 +75,39 @@ macro_rules! schema_for { #[macro_export] macro_rules! schema_for_value { ($value:expr) => { - $crate::gen::SchemaGenerator::default() + $crate::SchemaGenerator::default() .into_root_schema_for_value(&$value) .unwrap() }; } + +/// Construct a [`Schema`](crate::Schema) from a JSON literal. This can either be a JSON object, or +/// a boolean (`true` or `false`). +/// +/// You can interpolate variables or expressions into a JSON object using the same rules as the +/// [`serde_json::json`] macro. +/// +/// # Example +/// ``` +/// use schemars::{Schema, json_schema}; +/// +/// let desc = "A helpful description."; +/// let my_schema: Schema = json_schema!({ +/// "description": desc, +/// "type": ["object", "null"] +/// }); +/// ``` +#[macro_export] +macro_rules! json_schema { + ( + {$($json_object:tt)*} + ) => { + $crate::Schema::try_from($crate::_private::serde_json::json!({$($json_object)*})).unwrap() + }; + (true) => { + $crate::Schema::from(true) + }; + (false) => { + $crate::Schema::from(false) + }; +} diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 01fce865..a0028d21 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -2,550 +2,395 @@ JSON Schema types. */ -#[cfg(feature = "impl_json_schema")] -use crate as schemars; -#[cfg(feature = "impl_json_schema")] -use crate::JsonSchema; -use crate::{Map, Set}; +use crate::_alloc_prelude::*; +use ref_cast::{ref_cast_custom, RefCastCustom}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::ops::Deref; +use serde_json::{Map, Value}; /// A JSON Schema. -#[allow(clippy::large_enum_variant)] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(untagged)] -pub enum Schema { - /// A trivial boolean JSON Schema. - /// - /// The schema `true` matches everything (always passes validation), whereas the schema `false` - /// matches nothing (always fails validation). - Bool(bool), - /// A JSON Schema object. - Object(SchemaObject), +/// +/// This wraps a JSON [`Value`] that must be either an [object](Value::Object) or a +/// [bool](Value::Bool). +/// +/// A custom JSON schema can be created using the [`json_schema!`](crate::json_schema) macro: +/// ``` +/// use schemars::{Schema, json_schema}; +/// +/// let my_schema: Schema = json_schema!({ +/// "type": ["object", "null"] +/// }); +/// ``` +/// +/// Because a `Schema` is a thin wrapper around a `Value`, you can also use +/// [`TryFrom::try_from`]/[`TryInto::try_into`] to create a `Schema` from an existing `Value`. +/// This operation is fallible, because only [objects](Value::Object) and [bools](Value::Bool) can +/// be converted in this way. +/// +/// ``` +/// use schemars::{Schema, json_schema}; +/// use serde_json::json; +/// +/// let json_object = json!({"type": ["object", "null"]}); +/// let object_schema: Schema = json_object.try_into().unwrap(); +/// +/// let json_bool = json!(true); +/// let bool_schema: Schema = json_bool.try_into().unwrap(); +/// +/// let json_string = json!("This is neither an object nor a bool!"); +/// assert!(Schema::try_from(json_string).is_err()); +/// +/// // You can also convert a `&Value`/`&mut Value` to a `&Schema`/`&mut Schema` the same way: +/// +/// let json_object = json!({"type": ["object", "null"]}); +/// let object_schema_ref: &Schema = (&json_object).try_into().unwrap(); +/// +/// let mut json_object = json!({"type": ["object", "null"]}); +/// let object_schema_mut: &mut Schema = (&mut json_object).try_into().unwrap(); +/// ``` +/// +/// Similarly, you can use [`From`]/[`Into`] to (infallibly) create a `Schema` from an existing +/// [`Map`] or [`bool`]. +/// +/// ``` +/// use schemars::{Schema, json_schema}; +/// use serde_json::{Map, json}; +/// +/// let mut map = Map::new(); +/// map.insert("type".to_owned(), json!(["object", "null"])); +/// let object_schema: Schema = map.into(); +/// +/// let bool_schema: Schema = true.into(); +/// ``` +#[derive(Debug, Clone, PartialEq, RefCastCustom)] +#[repr(transparent)] +pub struct Schema(Value); + +impl<'de> Deserialize<'de> for Schema { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + Schema::validate(&value)?; + Ok(Schema(value)) + } +} + +impl Serialize for Schema { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + ser::OrderedKeywordWrapper(&self.0).serialize(serializer) + } } impl Schema { - /// Creates a new `$ref` schema. + /// Creates a new schema object with a single string property `"$ref"`. /// /// The given reference string should be a URI reference. This will usually be a JSON Pointer /// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6). pub fn new_ref(reference: String) -> Self { - SchemaObject::new_ref(reference).into() + let mut map = Map::new(); + map.insert("$ref".to_owned(), Value::String(reference)); + Self(Value::Object(map)) } - /// Returns `true` if `self` is a `$ref` schema. - /// - /// If `self` is a [`SchemaObject`] with `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`. - /// Otherwise, returns `false`. - pub fn is_ref(&self) -> bool { - match self { - Schema::Object(o) => o.is_ref(), - _ => false, + /// Borrows the `Schema`'s underlying JSON value. + pub fn as_value(&self) -> &Value { + &self.0 + } + + /// If the `Schema`'s underlying JSON value is a bool, returns the bool value. + pub fn as_bool(&self) -> Option { + self.0.as_bool() + } + + /// If the `Schema`'s underlying JSON value is an object, borrows the object as a `Map` of + /// properties. + pub fn as_object(&self) -> Option<&Map> { + self.0.as_object() + } + + /// If the `Schema`'s underlying JSON value is an object, mutably borrows the object as a `Map` + /// of properties. + pub fn as_object_mut(&mut self) -> Option<&mut Map> { + self.0.as_object_mut() + } + + pub(crate) fn try_to_object(self) -> Result, bool> { + match self.0 { + Value::Object(m) => Ok(m), + Value::Bool(b) => Err(b), + _ => unreachable!(), } } - /// Converts the given schema (if it is a boolean schema) into an equivalent schema object. + /// Returns the `Schema`'s underlying JSON value. + pub fn to_value(self) -> Value { + self.0 + } + + /// Converts the `Schema` (if it wraps a bool value) into an equivalent object schema. Then + /// mutably borrows the object as a `Map` of properties. + /// + /// `true` is transformed into an empty schema `{}`, which successfully validates against all + /// possible values. `false` is transformed into the schema `{"not": {}}`, which does not + /// successfully validate against any value. + #[allow(clippy::missing_panics_doc)] + pub fn ensure_object(&mut self) -> &mut Map { + if let Some(b) = self.as_bool() { + let mut map = Map::new(); + if !b { + map.insert("not".into(), Value::Object(Map::new())); + } + self.0 = Value::Object(map); + } + + self.0 + .as_object_mut() + .expect("Schema value should be of type Object.") + } + + /// Inserts a property into the schema, replacing any previous value. + /// + /// If the schema wraps a bool value, it will first be converted into an equivalent object + /// schema. + /// + /// If the schema did not have this key present, `None` is returned. /// - /// If the given schema is already a schema object, this has no effect. + /// If the schema did have this key present, the value is updated, and the old value is + /// returned. /// /// # Example /// ``` - /// use schemars::schema::{Schema, SchemaObject}; + /// use schemars::json_schema; + /// use serde_json::json; /// - /// let bool_schema = Schema::Bool(true); + /// let mut schema = json_schema!(true); + /// assert_eq!(schema.insert("type".to_owned(), "array".into()), None); + /// assert_eq!(schema.insert("type".to_owned(), "object".into()), Some(json!("array"))); /// - /// assert_eq!(bool_schema.into_object(), SchemaObject::default()); + /// assert_eq!(schema, json_schema!({"type": "object"})); /// ``` - pub fn into_object(self) -> SchemaObject { - match self { - Schema::Object(o) => o, - Schema::Bool(true) => SchemaObject::default(), - Schema::Bool(false) => SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - not: Some(Schema::Object(Default::default()).into()), - ..Default::default() - })), - ..Default::default() - }, - } - } -} - -impl From for Schema { - fn from(o: SchemaObject) -> Self { - Schema::Object(o) - } -} - -impl From for Schema { - fn from(b: bool) -> Self { - Schema::Bool(b) + pub fn insert(&mut self, k: String, v: Value) -> Option { + self.ensure_object().insert(k, v) } -} -/// The root object of a JSON Schema document. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct RootSchema { - /// The `$schema` keyword. - /// - /// See [JSON Schema 8.1.1. The "$schema" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1). - #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] - pub meta_schema: Option, - /// The root schema itself. - #[serde(flatten)] - pub schema: SchemaObject, - /// The `definitions` keyword. + /// If the `Schema`'s underlying JSON value is an object, gets a reference to that object's + /// value for the given key. /// - /// In JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still - /// serialized as `definitions` for backward-compatibility. + /// This always returns `None` for bool schemas. /// - /// See [JSON Schema 8.2.5. Schema Re-Use With "$defs"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5), - /// and [JSON Schema (draft 07) 9. Schema Re-Use With "definitions"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9). - #[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")] - pub definitions: Map, -} - -/// A JSON Schema object. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct SchemaObject { - /// Properties which annotate the [`SchemaObject`] which typically have no effect when an object is being validated against the schema. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub metadata: Option>, - /// The `type` keyword. + /// # Example + /// ``` + /// use schemars::json_schema; + /// use serde_json::json; /// - /// See [JSON Schema Validation 6.1.1. "type"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) - /// and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1). - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub instance_type: Option>, - /// The `format` keyword. + /// let obj_schema = json_schema!({"type": "array"}); + /// assert_eq!(obj_schema.get("type"), Some(&json!("array"))); + /// assert_eq!(obj_schema.get("format"), None); /// - /// See [JSON Schema Validation 7. A Vocabulary for Semantic Content With "format"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7). - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, - /// The `enum` keyword. + /// let bool_schema = json_schema!(true); + /// assert_eq!(bool_schema.get("type"), None); + /// ``` + pub fn get(&self, key: &Q) -> Option<&Value> + where + String: core::borrow::Borrow, + Q: ?Sized + Ord + Eq + core::hash::Hash, + { + self.0.as_object().and_then(|o| o.get(key)) + } + + /// If the `Schema`'s underlying JSON value is an object, removes and returns its value for the + /// given key. /// - /// See [JSON Schema Validation 6.1.2. "enum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2) - #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] - pub enum_values: Option>, - /// The `const` keyword. + /// This always returns `None` for bool schemas, without modifying them. /// - /// See [JSON Schema Validation 6.1.3. "const"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3) - #[serde( - rename = "const", - skip_serializing_if = "Option::is_none", - deserialize_with = "allow_null" - )] - pub const_value: Option, - /// Properties of the [`SchemaObject`] which define validation assertions in terms of other schemas. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub subschemas: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for numbers. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub number: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for strings. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub string: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for arrays. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub array: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for objects. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub object: Option>, - /// The `$ref` keyword. + /// # Example + /// ``` + /// use schemars::json_schema; + /// use serde_json::json; /// - /// See [JSON Schema 8.2.4.1. Direct References with "$ref"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1). - #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] - pub reference: Option, - /// Arbitrary extra properties which are not part of the JSON Schema specification, or which `schemars` does not support. - #[serde(flatten)] - pub extensions: Map, -} - -// Deserializing "null" to `Option` directly results in `None`, -// this function instead makes it deserialize to `Some(Value::Null)`. -fn allow_null<'de, D>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - Value::deserialize(de).map(Option::Some) -} - -fn skip_if_default<'de, D, T>(deserializer: D) -> Result>, D::Error> -where - D: serde::Deserializer<'de>, - T: Deserialize<'de> + Default + PartialEq, -{ - let value = T::deserialize(deserializer)?; - if value == T::default() { - Ok(None) - } else { - Ok(Some(Box::new(value))) + /// let mut schema = json_schema!({"type": "array"}); + /// assert_eq!(schema.remove("type"), Some(json!("array"))); + /// assert_eq!(schema, json_schema!({})); + /// ``` + pub fn remove(&mut self, key: &Q) -> Option + where + String: core::borrow::Borrow, + Q: ?Sized + Ord + Eq + core::hash::Hash, + { + self.0.as_object_mut().and_then(|o| o.remove(key)) } -} - -macro_rules! get_or_insert_default_fn { - ($name:ident, $ret:ty) => { - get_or_insert_default_fn!( - concat!( - "Returns a mutable reference to this schema's [`", - stringify!($ret), - "`](#structfield.", - stringify!($name), - "), creating it if it was `None`." - ), - $name, - $ret - ); - }; - ($doc:expr, $name:ident, $ret:ty) => { - #[doc = $doc] - pub fn $name(&mut self) -> &mut $ret { - self.$name.get_or_insert_with(Default::default) - } - }; -} -impl SchemaObject { - /// Creates a new `$ref` schema. - /// - /// The given reference string should be a URI reference. This will usually be a JSON Pointer - /// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6). - pub fn new_ref(reference: String) -> Self { - SchemaObject { - reference: Some(reference), - ..Default::default() + pub(crate) fn has_type(&self, ty: &str) -> bool { + match self.0.get("type") { + Some(Value::Array(values)) => values.iter().any(|v| v.as_str() == Some(ty)), + Some(Value::String(s)) => s == ty, + _ => false, } } - /// Returns `true` if `self` is a `$ref` schema. - /// - /// If `self` has `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`. - /// Otherwise, returns `false`. - pub fn is_ref(&self) -> bool { - self.reference.is_some() - } + fn validate(value: &Value) -> Result<(), E> { + use serde::de::Unexpected; + let unexpected = match value { + Value::Bool(_) | Value::Object(_) => return Ok(()), + Value::Null => Unexpected::Unit, + Value::Number(n) => { + if let Some(u) = n.as_u64() { + Unexpected::Unsigned(u) + } else if let Some(i) = n.as_i64() { + Unexpected::Signed(i) + } else if let Some(f) = n.as_f64() { + Unexpected::Float(f) + } else { + unreachable!() + } + } + Value::String(s) => Unexpected::Str(s), + Value::Array(_) => Unexpected::Seq, + }; - /// Returns `true` if `self` accepts values of the given type, according to the [`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 - /// to this method. - pub fn has_type(&self, ty: InstanceType) -> bool { - self.instance_type - .as_ref() - .map_or(true, |x| x.contains(&ty)) + Err(E::invalid_type(unexpected, &"object or boolean")) } - get_or_insert_default_fn!(metadata, Metadata); - get_or_insert_default_fn!(subschemas, SubschemaValidation); - get_or_insert_default_fn!(number, NumberValidation); - get_or_insert_default_fn!(string, StringValidation); - get_or_insert_default_fn!(array, ArrayValidation); - get_or_insert_default_fn!(object, ObjectValidation); + #[allow(unsafe_code)] + #[ref_cast_custom] + fn ref_cast(value: &Value) -> &Self; + + #[allow(unsafe_code)] + #[ref_cast_custom] + fn ref_cast_mut(value: &mut Value) -> &mut Self; } -impl From for SchemaObject { - fn from(schema: Schema) -> Self { - schema.into_object() +impl From for Value { + fn from(v: Schema) -> Value { + v.0 } } -/// Properties which annotate a [`SchemaObject`] which typically have no effect when an object is being validated against the schema. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct Metadata { - /// The `$id` keyword. - /// - /// See [JSON Schema 8.2.2. The "$id" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2). - #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] - pub id: Option, - /// The `title` keyword. - /// - /// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// The `description` keyword. - /// - /// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// The `default` keyword. - /// - /// See [JSON Schema Validation 9.2. "default"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2). - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "allow_null" - )] - pub default: Option, - /// The `deprecated` keyword. - /// - /// See [JSON Schema Validation 9.3. "deprecated"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3). - #[serde(skip_serializing_if = "is_false")] - pub deprecated: bool, - /// The `readOnly` keyword. - /// - /// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4). - #[serde(skip_serializing_if = "is_false")] - pub read_only: bool, - /// The `writeOnly` keyword. - /// - /// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4). - #[serde(skip_serializing_if = "is_false")] - pub write_only: bool, - /// The `examples` keyword. - /// - /// See [JSON Schema Validation 9.5. "examples"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5). - #[serde(skip_serializing_if = "Vec::is_empty")] - pub examples: Vec, -} +impl core::convert::TryFrom for Schema { + type Error = serde_json::Error; -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_false(b: &bool) -> bool { - !b + fn try_from(value: Value) -> serde_json::Result { + Schema::validate(&value)?; + Ok(Schema(value)) + } } -/// Properties of a [`SchemaObject`] which define validation assertions in terms of other schemas. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct SubschemaValidation { - /// The `allOf` keyword. - /// - /// See [JSON Schema 9.2.1.1. "allOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub all_of: Option>, - /// The `anyOf` keyword. - /// - /// See [JSON Schema 9.2.1.2. "anyOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub any_of: Option>, - /// The `oneOf` keyword. - /// - /// See [JSON Schema 9.2.1.3. "oneOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub one_of: Option>, - /// The `not` keyword. - /// - /// See [JSON Schema 9.2.1.4. "not"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub not: Option>, - /// The `if` keyword. - /// - /// See [JSON Schema 9.2.2.1. "if"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1). - #[serde(rename = "if", skip_serializing_if = "Option::is_none")] - pub if_schema: Option>, - /// The `then` keyword. - /// - /// See [JSON Schema 9.2.2.2. "then"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2). - #[serde(rename = "then", skip_serializing_if = "Option::is_none")] - pub then_schema: Option>, - /// The `else` keyword. - /// - /// See [JSON Schema 9.2.2.3. "else"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3). - #[serde(rename = "else", skip_serializing_if = "Option::is_none")] - pub else_schema: Option>, -} +impl<'a> core::convert::TryFrom<&'a Value> for &'a Schema { + type Error = serde_json::Error; -/// Properties of a [`SchemaObject`] which define validation assertions for numbers. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct NumberValidation { - /// The `multipleOf` keyword. - /// - /// See [JSON Schema Validation 6.2.1. "multipleOf"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub multiple_of: Option, - /// The `maximum` keyword. - /// - /// See [JSON Schema Validation 6.2.2. "maximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub maximum: Option, - /// The `exclusiveMaximum` keyword. - /// - /// See [JSON Schema Validation 6.2.3. "exclusiveMaximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub exclusive_maximum: Option, - /// The `minimum` keyword. - /// - /// See [JSON Schema Validation 6.2.4. "minimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum: Option, - /// The `exclusiveMinimum` keyword. - /// - /// See [JSON Schema Validation 6.2.5. "exclusiveMinimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5). - #[serde(skip_serializing_if = "Option::is_none")] - pub exclusive_minimum: Option, + fn try_from(value: &Value) -> serde_json::Result<&Schema> { + Schema::validate(value)?; + Ok(Schema::ref_cast(value)) + } } -/// Properties of a [`SchemaObject`] which define validation assertions for strings. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct StringValidation { - /// The `maxLength` keyword. - /// - /// See [JSON Schema Validation 6.3.1. "maxLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_length: Option, - /// The `minLength` keyword. - /// - /// See [JSON Schema Validation 6.3.2. "minLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_length: Option, - /// The `pattern` keyword. - /// - /// See [JSON Schema Validation 6.3.3. "pattern"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub pattern: Option, -} +impl<'a> core::convert::TryFrom<&'a mut Value> for &'a mut Schema { + type Error = serde_json::Error; -/// Properties of a [`SchemaObject`] which define validation assertions for arrays. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct ArrayValidation { - /// The `items` keyword. - /// - /// See [JSON Schema 9.3.1.1. "items"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>, - /// The `additionalItems` keyword. - /// - /// See [JSON Schema 9.3.1.2. "additionalItems"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_items: Option>, - /// The `maxItems` keyword. - /// - /// See [JSON Schema Validation 6.4.1. "maxItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_items: Option, - /// The `minItems` keyword. - /// - /// See [JSON Schema Validation 6.4.2. "minItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_items: Option, - /// The `uniqueItems` keyword. - /// - /// See [JSON Schema Validation 6.4.3. "uniqueItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub unique_items: Option, - /// The `contains` keyword. - /// - /// See [JSON Schema 9.3.1.4. "contains"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub contains: Option>, + fn try_from(value: &mut Value) -> serde_json::Result<&mut Schema> { + Schema::validate(value)?; + Ok(Schema::ref_cast_mut(value)) + } } -/// Properties of a [`SchemaObject`] which define validation assertions for objects. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct ObjectValidation { - /// The `maxProperties` keyword. - /// - /// See [JSON Schema Validation 6.5.1. "maxProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_properties: Option, - /// The `minProperties` keyword. - /// - /// See [JSON Schema Validation 6.5.2. "minProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_properties: Option, - /// The `required` keyword. - /// - /// See [JSON Schema Validation 6.5.3. "required"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3). - #[serde(skip_serializing_if = "Set::is_empty")] - pub required: Set, - /// The `properties` keyword. - /// - /// See [JSON Schema 9.3.2.1. "properties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1). - #[serde(skip_serializing_if = "Map::is_empty")] - pub properties: Map, - /// The `patternProperties` keyword. - /// - /// See [JSON Schema 9.3.2.2. "patternProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2). - #[serde(skip_serializing_if = "Map::is_empty")] - pub pattern_properties: Map, - /// The `additionalProperties` keyword. - /// - /// See [JSON Schema 9.3.2.3. "additionalProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_properties: Option>, - /// The `propertyNames` keyword. - /// - /// See [JSON Schema 9.3.2.5. "propertyNames"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5). - #[serde(skip_serializing_if = "Option::is_none")] - pub property_names: Option>, +impl Default for Schema { + fn default() -> Self { + Self(Value::Object(Map::new())) + } } -/// The possible types of values in JSON Schema documents. -/// -/// See [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1). -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase")] -pub enum InstanceType { - Null, - Boolean, - Object, - Array, - Number, - String, - Integer, +impl From> for Schema { + fn from(o: Map) -> Self { + Schema(Value::Object(o)) + } } -/// A type which can be serialized as a single item, or multiple items. -/// -/// In some contexts, a `Single` may be semantically distinct from a `Vec` containing only item. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(untagged)] -pub enum SingleOrVec { - Single(Box), - Vec(Vec), +impl From for Schema { + fn from(b: bool) -> Self { + Schema(Value::Bool(b)) + } } -impl From for SingleOrVec { - fn from(single: T) -> Self { - SingleOrVec::Single(Box::new(single)) +impl crate::JsonSchema for Schema { + fn schema_name() -> alloc::borrow::Cow<'static, str> { + "Schema".into() } -} -impl From> for SingleOrVec { - fn from(vec: Vec) -> Self { - SingleOrVec::Vec(vec) + fn schema_id() -> alloc::borrow::Cow<'static, str> { + "schemars::Schema".into() + } + + fn json_schema(_: &mut crate::SchemaGenerator) -> Schema { + crate::json_schema!({ + "type": ["object", "boolean"] + }) } } -impl SingleOrVec { - /// Returns `true` if `self` is either a `Single` equal to `x`, or a `Vec` containing `x`. - /// - /// # Examples - /// - /// ``` - /// use schemars::schema::SingleOrVec; - /// - /// let s = SingleOrVec::from(10); - /// assert!(s.contains(&10)); - /// assert!(!s.contains(&20)); - /// - /// let v = SingleOrVec::from(vec![10, 20]); - /// assert!(v.contains(&10)); - /// assert!(v.contains(&20)); - /// assert!(!v.contains(&30)); - /// ``` - pub fn contains(&self, x: &T) -> bool { - match self { - SingleOrVec::Single(s) => s.deref() == x, - SingleOrVec::Vec(v) => v.contains(x), +mod ser { + use serde::ser::{Serialize, SerializeMap, SerializeSeq}; + use serde_json::Value; + + // The order of properties in a JSON Schema object is insignificant, but we explicitly order + // some of them here to make them easier for a human to read. All other properties are ordered + // either lexicographically (by default) or by insertion order (if `preserve_order` is enabled) + const ORDERED_KEYWORDS_START: [&str; 7] = [ + "$id", + "$schema", + "title", + "description", + "type", + "format", + "properties", + ]; + const ORDERED_KEYWORDS_END: [&str; 2] = ["$defs", "definitions"]; + + pub(super) struct OrderedKeywordWrapper<'a>(pub &'a Value); + + impl Serialize for OrderedKeywordWrapper<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + Value::Array(array) => { + let mut seq = serializer.serialize_seq(Some(array.len()))?; + for value in array { + seq.serialize_element(&OrderedKeywordWrapper(value))?; + } + seq.end() + } + Value::Object(object) => { + let mut map = serializer.serialize_map(Some(object.len()))?; + + for key in ORDERED_KEYWORDS_START { + if let Some(value) = object.get(key) { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + for (key, value) in object { + if !ORDERED_KEYWORDS_START.contains(&key.as_str()) + && !ORDERED_KEYWORDS_END.contains(&key.as_str()) + { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + for key in ORDERED_KEYWORDS_END { + if let Some(value) = object.get(key) { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + map.end() + } + _ => self.0.serialize(serializer), + } } } } diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs index 3f69bef5..079c566d 100644 --- a/schemars/src/ser.rs +++ b/schemars/src/ser.rs @@ -1,28 +1,27 @@ -use crate::schema::*; -use crate::JsonSchema; -use crate::{gen::SchemaGenerator, Map}; -use serde_json::{Error, Value}; -use std::{convert::TryInto, fmt::Display}; +use crate::_alloc_prelude::*; +use crate::{json_schema, JsonSchema, Schema, SchemaGenerator}; +use core::fmt::Display; +use serde_json::{Error, Map, Value}; pub(crate) struct Serializer<'a> { - pub(crate) gen: &'a mut SchemaGenerator, + pub(crate) generator: &'a mut SchemaGenerator, pub(crate) include_title: bool, } pub(crate) struct SerializeSeq<'a> { - gen: &'a mut SchemaGenerator, + generator: &'a mut SchemaGenerator, items: Option, } pub(crate) struct SerializeTuple<'a> { - gen: &'a mut SchemaGenerator, + generator: &'a mut SchemaGenerator, items: Vec, title: &'static str, } pub(crate) struct SerializeMap<'a> { - gen: &'a mut SchemaGenerator, - properties: Map, + generator: &'a mut SchemaGenerator, + properties: Map, current_key: Option, title: &'static str, } @@ -30,19 +29,17 @@ pub(crate) struct SerializeMap<'a> { macro_rules! forward_to_subschema_for { ($fn:ident, $ty:ty) => { fn $fn(self, _value: $ty) -> Result { - Ok(self.gen.subschema_for::<$ty>()) + Ok(self.generator.subschema_for::<$ty>()) } }; } macro_rules! return_instance_type { - ($fn:ident, $ty:ty, $instance_type:ident) => { + ($fn:ident, $ty:ty, $instance_type:expr) => { fn $fn(self, _value: $ty) -> Result { - Ok(SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - ..Default::default() - } - .into()) + Ok(json_schema!({ + "type": $instance_type + })) } }; } @@ -59,29 +56,29 @@ impl<'a> serde::Serializer for Serializer<'a> { type SerializeStruct = SerializeMap<'a>; type SerializeStructVariant = Self; - return_instance_type!(serialize_i8, i8, Integer); - return_instance_type!(serialize_i16, i16, Integer); - return_instance_type!(serialize_i32, i32, Integer); - return_instance_type!(serialize_i64, i64, Integer); - return_instance_type!(serialize_i128, i128, Integer); - return_instance_type!(serialize_u8, u8, Integer); - return_instance_type!(serialize_u16, u16, Integer); - return_instance_type!(serialize_u32, u32, Integer); - return_instance_type!(serialize_u64, u64, Integer); - return_instance_type!(serialize_u128, u128, Integer); - return_instance_type!(serialize_f32, f32, Number); - return_instance_type!(serialize_f64, f64, Number); + return_instance_type!(serialize_i8, i8, "integer"); + return_instance_type!(serialize_i16, i16, "integer"); + return_instance_type!(serialize_i32, i32, "integer"); + return_instance_type!(serialize_i64, i64, "integer"); + return_instance_type!(serialize_i128, i128, "integer"); + return_instance_type!(serialize_u8, u8, "integer"); + return_instance_type!(serialize_u16, u16, "integer"); + return_instance_type!(serialize_u32, u32, "integer"); + return_instance_type!(serialize_u64, u64, "integer"); + return_instance_type!(serialize_u128, u128, "integer"); + return_instance_type!(serialize_f32, f32, "number"); + return_instance_type!(serialize_f64, f64, "number"); forward_to_subschema_for!(serialize_bool, bool); forward_to_subschema_for!(serialize_char, char); forward_to_subschema_for!(serialize_str, &str); forward_to_subschema_for!(serialize_bytes, &[u8]); - fn collect_str(self, _value: &T) -> Result + fn collect_str(self, _value: &T) -> Result where - T: Display, + T: Display + ?Sized, { - Ok(self.gen.subschema_for::<&str>()) + Ok(self.generator.subschema_for::<&str>()) } fn collect_map(self, iter: I) -> Result @@ -93,98 +90,90 @@ impl<'a> serde::Serializer for Serializer<'a> { let value_schema = iter .into_iter() .try_fold(None, |acc, (_, v)| { - if acc == Some(Schema::Bool(true)) { + if acc == Some(true.into()) { return Ok(acc); } let schema = v.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; Ok(match &acc { None => Some(schema), - Some(items) if items != &schema => Some(Schema::Bool(true)), + Some(items) if items != &schema => Some(true.into()), _ => acc, }) })? - .unwrap_or(Schema::Bool(true)); - - Ok(SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(value_schema)), - ..ObjectValidation::default() - })), - ..SchemaObject::default() - } - .into()) + .unwrap_or(true.into()); + + Ok(json_schema!({ + "type": "object", + "additionalProperties": value_schema, + })) } fn serialize_none(self) -> Result { - Ok(self.gen.subschema_for::>()) + Ok(self.generator.subschema_for::>()) } fn serialize_unit(self) -> Result { self.serialize_none() } - fn serialize_some(self, value: &T) -> Result + fn serialize_some(self, value: &T) -> Result where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { - // FIXME nasty duplication of `impl JsonSchema for Option` - fn add_null_type(instance_type: &mut SingleOrVec) { - match instance_type { - SingleOrVec::Single(ty) if **ty != InstanceType::Null => { - *instance_type = vec![**ty, InstanceType::Null].into() - } - SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => { - ty.push(InstanceType::Null) - } - _ => {} - }; - } - let mut schema = value.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; - if self.gen.settings().option_add_null_type { - schema = match schema { - Schema::Bool(true) => Schema::Bool(true), - Schema::Bool(false) => <()>::json_schema(self.gen), - Schema::Object(SchemaObject { - instance_type: Some(ref mut instance_type), - .. - }) => { - add_null_type(instance_type); - schema - } - schema => SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - any_of: Some(vec![schema, <()>::json_schema(self.gen)]), - ..Default::default() - })), - ..Default::default() + if self.generator.settings().option_add_null_type { + schema = match schema.try_to_object() { + Ok(mut obj) => { + let value = obj.get_mut("type"); + match value { + Some(Value::Array(array)) => { + let null = Value::from("null"); + if !array.contains(&null) { + array.push(null); + } + obj.into() + } + Some(Value::String(string)) => { + if string != "null" { + *value.unwrap() = Value::Array(vec![ + core::mem::take(string).into(), + "null".into(), + ]); + } + obj.into() + } + _ => json_schema!({ + "anyOf": [ + obj, + <()>::json_schema(self.generator) + ] + }), + } } - .into(), + Err(true) => true.into(), + Err(false) => <()>::json_schema(self.generator), } } - if self.gen.settings().option_nullable { - let mut schema_obj = schema.into_object(); - schema_obj - .extensions - .insert("nullable".to_owned(), serde_json::json!(true)); - schema = Schema::Object(schema_obj); + if self.generator.settings().option_nullable { + schema + .ensure_object() + .insert("nullable".into(), true.into()); }; Ok(schema) } fn serialize_unit_struct(self, _name: &'static str) -> Result { - Ok(self.gen.subschema_for::<()>()) + Ok(self.generator.subschema_for::<()>()) } fn serialize_unit_variant( @@ -193,30 +182,28 @@ impl<'a> serde::Serializer for Serializer<'a> { _variant_index: u32, _variant: &'static str, ) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } - fn serialize_newtype_struct( + fn serialize_newtype_struct( self, name: &'static str, value: &T, ) -> Result where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { let include_title = self.include_title; - let mut result = value.serialize(self); + let mut schema = value.serialize(self)?; - if include_title { - if let Ok(Schema::Object(ref mut object)) = result { - object.metadata().title = Some(name.to_string()); - } + if include_title && !name.is_empty() { + schema.ensure_object().insert("title".into(), name.into()); } - result + Ok(schema) } - fn serialize_newtype_variant( + fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, @@ -224,21 +211,21 @@ impl<'a> serde::Serializer for Serializer<'a> { _value: &T, ) -> Result where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { - Ok(Schema::Bool(true)) + Ok(true.into()) } fn serialize_seq(self, _len: Option) -> Result { Ok(SerializeSeq { - gen: self.gen, + generator: self.generator, items: None, }) } fn serialize_tuple(self, len: usize) -> Result { Ok(SerializeTuple { - gen: self.gen, + generator: self.generator, items: Vec::with_capacity(len), title: "", }) @@ -251,7 +238,7 @@ impl<'a> serde::Serializer for Serializer<'a> { ) -> Result { let title = if self.include_title { name } else { "" }; Ok(SerializeTuple { - gen: self.gen, + generator: self.generator, items: Vec::with_capacity(len), title, }) @@ -269,7 +256,7 @@ impl<'a> serde::Serializer for Serializer<'a> { fn serialize_map(self, _len: Option) -> Result { Ok(SerializeMap { - gen: self.gen, + generator: self.generator, properties: Map::new(), current_key: None, title: "", @@ -283,7 +270,7 @@ impl<'a> serde::Serializer for Serializer<'a> { ) -> Result { let title = if self.include_title { name } else { "" }; Ok(SerializeMap { - gen: self.gen, + generator: self.generator, properties: Map::new(), current_key: None, title, @@ -305,15 +292,15 @@ impl serde::ser::SerializeTupleVariant for Serializer<'_> { type Ok = Schema; type Error = Error; - fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { Ok(()) } fn end(self) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } } @@ -321,19 +308,15 @@ impl serde::ser::SerializeStructVariant for Serializer<'_> { type Ok = Schema; type Error = Error; - fn serialize_field( - &mut self, - _key: &'static str, - _value: &T, - ) -> Result<(), Self::Error> + fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { Ok(()) } fn end(self) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } } @@ -341,20 +324,20 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> { type Ok = Schema; type Error = Error; - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { - if self.items != Some(Schema::Bool(true)) { + if self.items != Some(true.into()) { let schema = value.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; match &self.items { None => self.items = Some(schema), Some(items) => { if items != &schema { - self.items = Some(Schema::Bool(true)) + self.items = Some(true.into()); } } } @@ -364,16 +347,12 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> { } fn end(self) -> Result { - let items = self.items.unwrap_or(Schema::Bool(true)); - Ok(SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(items.into()), - ..ArrayValidation::default() - })), - ..SchemaObject::default() - } - .into()) + let items = self.items.unwrap_or(true.into()); + + Ok(json_schema!({ + "type": "array", + "items": items + })) } } @@ -381,12 +360,12 @@ impl serde::ser::SerializeTuple for SerializeTuple<'_> { type Ok = Schema; type Error = Error; - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { let schema = value.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; self.items.push(schema); @@ -394,23 +373,21 @@ impl serde::ser::SerializeTuple for SerializeTuple<'_> { } fn end(self) -> Result { - let len = self.items.len().try_into().ok(); - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(SingleOrVec::Vec(self.items)), - max_items: len, - min_items: len, - ..ArrayValidation::default() - })), - ..SchemaObject::default() - }; + let len = self.items.len(); + let mut schema = json_schema!({ + "type": "array", + "prefixItems": self.items, + "maxItems": len, + "minItems": len, + }); if !self.title.is_empty() { - schema.metadata().title = Some(self.title.to_owned()); + schema + .ensure_object() + .insert("title".into(), self.title.into()); } - Ok(schema.into()) + Ok(schema) } } @@ -418,9 +395,9 @@ impl serde::ser::SerializeTupleStruct for SerializeTuple<'_> { type Ok = Schema; type Error = Error; - fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { serde::ser::SerializeTuple::serialize_element(self, value) } @@ -434,9 +411,9 @@ impl serde::ser::SerializeMap for SerializeMap<'_> { type Ok = Schema; type Error = Error; - fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { // FIXME this is too lenient - we should return an error if serde_json // doesn't allow T to be a key of a map. @@ -450,35 +427,33 @@ impl serde::ser::SerializeMap for SerializeMap<'_> { Ok(()) } - fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { let key = self.current_key.take().unwrap_or_default(); let schema = value.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; - self.properties.insert(key, schema); + self.properties.insert(key, schema.into()); Ok(()) } fn end(self) -> Result { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - properties: self.properties, - ..ObjectValidation::default() - })), - ..SchemaObject::default() - }; + let mut schema = json_schema!({ + "type": "object", + "properties": self.properties, + }); if !self.title.is_empty() { - schema.metadata().title = Some(self.title.to_owned()); + schema + .ensure_object() + .insert("title".into(), self.title.into()); } - Ok(schema.into()) + Ok(schema) } } @@ -486,19 +461,15 @@ impl serde::ser::SerializeStruct for SerializeMap<'_> { type Ok = Schema; type Error = Error; - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> where - T: serde::Serialize, + T: serde::Serialize + ?Sized, { let prop_schema = value.serialize(Serializer { - gen: self.gen, + generator: self.generator, include_title: false, })?; - self.properties.insert(key.to_string(), prop_schema); + self.properties.insert(key.to_string(), prop_schema.into()); Ok(()) } diff --git a/schemars/src/transform.rs b/schemars/src/transform.rs new file mode 100644 index 00000000..047a62f3 --- /dev/null +++ b/schemars/src/transform.rs @@ -0,0 +1,444 @@ +/*! +Contains the [`Transform`] trait, used to modify a constructed schema and optionally its subschemas. +This trait is automatically implemented for functions of the form `fn(&mut Schema) -> ()`. + +# Recursive Transforms + +To make a transform recursive (i.e. apply it to subschemas), you have two options: +1. call the [`transform_subschemas`] function within the transform function +2. wrap the `Transform` in a [`RecursiveTransform`] + +# Examples + +To add a custom property to all object schemas: + +``` +# use schemars::{Schema, json_schema}; +use schemars::transform::{Transform, transform_subschemas}; + +pub struct MyTransform; + +impl Transform for MyTransform { + fn transform(&mut self, schema: &mut Schema) { + // First, make our change to this schema + schema.insert("my_property".to_string(), "hello world".into()); + + // Then apply the transform to any subschemas + transform_subschemas(self, schema); + } +} + +let mut schema = json_schema!({ + "type": "array", + "items": {} +}); + +MyTransform.transform(&mut schema); + +assert_eq!( + schema, + json_schema!({ + "type": "array", + "items": { + "my_property": "hello world" + }, + "my_property": "hello world" + }) +); +``` + +The same example with a `fn` transform: +``` +# use schemars::{Schema, json_schema}; +use schemars::transform::transform_subschemas; + +fn add_property(schema: &mut Schema) { + schema.insert("my_property".to_string(), "hello world".into()); + + transform_subschemas(&mut add_property, schema) +} + +let mut schema = json_schema!({ + "type": "array", + "items": {} +}); + +add_property(&mut schema); + +assert_eq!( + schema, + json_schema!({ + "type": "array", + "items": { + "my_property": "hello world" + }, + "my_property": "hello world" + }) +); +``` + +And the same example using a closure wrapped in a `RecursiveTransform`: +``` +# use schemars::{Schema, json_schema}; +use schemars::transform::{Transform, RecursiveTransform}; + +let mut transform = RecursiveTransform(|schema: &mut Schema| { + schema.insert("my_property".to_string(), "hello world".into()); +}); + +let mut schema = json_schema!({ + "type": "array", + "items": {} +}); + +transform.transform(&mut schema); + +assert_eq!( + schema, + json_schema!({ + "type": "array", + "items": { + "my_property": "hello world" + }, + "my_property": "hello world" + }) +); +``` + +*/ +use crate::Schema; +use crate::_alloc_prelude::*; +use alloc::collections::BTreeSet; +use serde_json::{json, Map, Value}; + +/// Trait used to modify a constructed schema and optionally its subschemas. +/// +/// See the [module documentation](self) for more details on implementing this trait. +pub trait Transform { + /// Applies the transform to the given [`Schema`]. + /// + /// When overriding this method, you may want to call the [`transform_subschemas`] function to + /// also transform any subschemas. + fn transform(&mut self, schema: &mut Schema); + + // Not public API + // Hack to enable implementing Debug on Box even though closures don't + // implement Debug + #[doc(hidden)] + fn _debug_type_name(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(core::any::type_name::()) + } +} + +impl Transform for F +where + F: FnMut(&mut Schema), +{ + fn transform(&mut self, schema: &mut Schema) { + self(schema); + } +} + +/// Applies the given [`Transform`] to all direct subschemas of the [`Schema`]. +pub fn transform_subschemas(t: &mut T, schema: &mut Schema) { + for (key, value) in schema.as_object_mut().into_iter().flatten() { + // This is intentionally written to work with multiple JSON Schema versions, so that + // users can add their own transforms on the end of e.g. `SchemaSettings::draft07()` and + // they will still apply to all subschemas "as expected". + // This is why this match statement contains both `additionalProperties` (which was + // dropped in draft 2020-12) and `prefixItems` (which was added in draft 2020-12). + match key.as_str() { + "not" + | "if" + | "then" + | "else" + | "contains" + | "additionalProperties" + | "propertyNames" + | "additionalItems" => { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + "allOf" | "anyOf" | "oneOf" | "prefixItems" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + } + } + // Support `items` array even though this is not allowed in draft 2020-12 (see above + // comment) + "items" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + } else if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + "properties" | "patternProperties" | "$defs" | "definitions" => { + if let Some(obj) = value.as_object_mut() { + for value in obj.values_mut() { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + } + } + _ => {} + } + } +} + +// Similar to `transform_subschemas`, but only transforms subschemas that apply to the top-level +// object, e.g. "oneOf" but not "properties". +pub(crate) fn transform_immediate_subschemas( + t: &mut T, + schema: &mut Schema, +) { + for (key, value) in schema.as_object_mut().into_iter().flatten() { + match key.as_str() { + "if" | "then" | "else" => { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + "allOf" | "anyOf" | "oneOf" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = value.try_into() { + t.transform(subschema); + } + } + } + } + _ => {} + } + } +} + +/// A helper struct that can wrap a non-recursive [`Transform`] (i.e. one that does not apply to +/// subschemas) into a recursive one. +/// +/// Its implementation of `Transform` will first apply the inner transform to the "parent" schema, +/// and then its subschemas (and their subschemas, and so on). +/// +/// # Example +/// ``` +/// # use schemars::{Schema, json_schema}; +/// use schemars::transform::{Transform, RecursiveTransform}; +/// +/// let mut transform = RecursiveTransform(|schema: &mut Schema| { +/// schema.insert("my_property".to_string(), "hello world".into()); +/// }); +/// +/// let mut schema = json_schema!({ +/// "type": "array", +/// "items": {} +/// }); +/// +/// transform.transform(&mut schema); +/// +/// assert_eq!( +/// schema, +/// json_schema!({ +/// "type": "array", +/// "items": { +/// "my_property": "hello world" +/// }, +/// "my_property": "hello world" +/// }) +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct RecursiveTransform(pub T); + +impl Transform for RecursiveTransform +where + T: Transform, +{ + fn transform(&mut self, schema: &mut Schema) { + self.0.transform(schema); + transform_subschemas(self, schema); + } +} + +/// Replaces boolean JSON Schemas with equivalent object schemas. +/// This also applies to subschemas. +/// +/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as +/// schemas. +#[derive(Debug, Clone)] +pub struct ReplaceBoolSchemas { + /// When set to `true`, a schema's `additionalProperties` property will not be changed from a + /// boolean. + pub skip_additional_properties: bool, +} + +impl Transform for ReplaceBoolSchemas { + fn transform(&mut self, schema: &mut Schema) { + if let Some(obj) = schema.as_object_mut() { + if self.skip_additional_properties { + if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") { + transform_subschemas(self, schema); + + schema.insert(ap_key, ap_value); + + return; + } + } + + transform_subschemas(self, schema); + } else { + schema.ensure_object(); + } + } +} + +/// Restructures JSON Schema objects so that the `$ref` property will never appear alongside any +/// other properties. This also applies to subschemas. +/// +/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support other properties +/// alongside `$ref`. +#[derive(Debug, Clone)] +pub struct RemoveRefSiblings; + +impl Transform for RemoveRefSiblings { + fn transform(&mut self, schema: &mut Schema) { + transform_subschemas(self, schema); + + if let Some(obj) = schema.as_object_mut().filter(|o| o.len() > 1) { + if let Some(ref_value) = obj.remove("$ref") { + if let Value::Array(all_of) = obj.entry("allOf").or_insert(Value::Array(Vec::new())) + { + all_of.push(json!({ + "$ref": ref_value + })); + } + } + } + } +} + +/// Removes the `examples` schema property and (if present) set its first value as the `example` +/// property. This also applies to subschemas. +/// +/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` +/// property. +#[derive(Debug, Clone)] +pub struct SetSingleExample; + +impl Transform for SetSingleExample { + fn transform(&mut self, schema: &mut Schema) { + transform_subschemas(self, schema); + + if let Some(Value::Array(examples)) = schema.remove("examples") { + if let Some(first_example) = examples.into_iter().next() { + schema.insert("example".into(), first_example); + } + } + } +} + +/// Replaces the `const` schema property with a single-valued `enum` property. +/// This also applies to subschemas. +/// +/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `const` +/// property. +#[derive(Debug, Clone)] +pub struct ReplaceConstValue; + +impl Transform for ReplaceConstValue { + fn transform(&mut self, schema: &mut Schema) { + transform_subschemas(self, schema); + + if let Some(value) = schema.remove("const") { + schema.insert("enum".into(), Value::Array(vec![value])); + } + } +} + +/// Rename the `prefixItems` schema property to `items`. +/// This also applies to subschemas. +/// +/// If the schema contains both `prefixItems` and `items`, then this additionally renames `items` to +/// `additionalItems`. +/// +/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the `prefixItems` +/// property. +#[derive(Debug, Clone)] +pub struct ReplacePrefixItems; + +impl Transform for ReplacePrefixItems { + fn transform(&mut self, schema: &mut Schema) { + transform_subschemas(self, schema); + + if let Some(prefix_items) = schema.remove("prefixItems") { + let previous_items = schema.insert("items".to_owned(), prefix_items); + + if let Some(previous_items) = previous_items { + schema.insert("additionalItems".to_owned(), previous_items); + } + } + } +} + +#[derive(Debug, Clone)] +pub struct ReplaceUnevaluatedProperties; + +impl Transform for ReplaceUnevaluatedProperties { + fn transform(&mut self, schema: &mut Schema) { + transform_subschemas(self, schema); + + let Some(up) = schema.remove("unevaluatedProperties") else { + return; + }; + + schema.insert("additionalProperties".to_owned(), up); + + let mut gather_property_names = GatherPropertyNames::default(); + gather_property_names.transform(schema); + let property_names = gather_property_names.0; + + if property_names.is_empty() { + return; + } + + if let Some(properties) = schema + .ensure_object() + .entry("properties") + .or_insert(Map::new().into()) + .as_object_mut() + { + for name in property_names { + properties.entry(name).or_insert(true.into()); + } + } + } +} + +// Helper for getting property names for all *immediate* subschemas +#[derive(Default)] +struct GatherPropertyNames(BTreeSet); + +impl Transform for GatherPropertyNames { + fn transform(&mut self, schema: &mut Schema) { + self.0.extend( + schema + .as_object() + .iter() + .filter_map(|o| o.get("properties")) + .filter_map(Value::as_object) + .flat_map(Map::keys) + .cloned(), + ); + + transform_immediate_subschemas(self, schema); + } +} diff --git a/schemars/src/visit.rs b/schemars/src/visit.rs deleted file mode 100644 index 182b5722..00000000 --- a/schemars/src/visit.rs +++ /dev/null @@ -1,212 +0,0 @@ -/*! -Contains the [`Visitor`] trait, used to recursively modify a constructed schema and its subschemas. - -Sometimes you may want to apply a change to a schema, as well as all schemas contained within it. -The easiest way to achieve this is by defining a type that implements [`Visitor`]. -All methods of `Visitor` have a default implementation that makes no change but recursively visits all subschemas. -When overriding one of these methods, you will *usually* want to still call this default implementation. - -# Example -To add a custom property to all schemas: -``` -use schemars::schema::SchemaObject; -use schemars::visit::{Visitor, visit_schema_object}; - -pub struct MyVisitor; - -impl Visitor for MyVisitor { - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - // First, make our change to this schema - schema.extensions.insert("my_property".to_string(), serde_json::json!("hello world")); - - // Then delegate to default implementation to visit any subschemas - visit_schema_object(self, schema); - } -} -``` -*/ -use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec}; - -/// Trait used to recursively modify a constructed schema and its subschemas. -pub trait Visitor { - /// Override this method to modify a [`RootSchema`] and (optionally) its subschemas. - /// - /// When overriding this method, you will usually want to call the [`visit_root_schema`] function to visit subschemas. - fn visit_root_schema(&mut self, root: &mut RootSchema) { - visit_root_schema(self, root) - } - - /// Override this method to modify a [`Schema`] and (optionally) its subschemas. - /// - /// When overriding this method, you will usually want to call the [`visit_schema`] function to visit subschemas. - fn visit_schema(&mut self, schema: &mut Schema) { - visit_schema(self, schema) - } - - /// Override this method to modify a [`SchemaObject`] and (optionally) its subschemas. - /// - /// When overriding this method, you will usually want to call the [`visit_schema_object`] function to visit subschemas. - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - visit_schema_object(self, schema) - } -} - -/// Visits all subschemas of the [`RootSchema`]. -pub fn visit_root_schema(v: &mut V, root: &mut RootSchema) { - v.visit_schema_object(&mut root.schema); - visit_map_values(v, &mut root.definitions); -} - -/// Visits all subschemas of the [`Schema`]. -pub fn visit_schema(v: &mut V, schema: &mut Schema) { - if let Schema::Object(schema) = schema { - v.visit_schema_object(schema) - } -} - -/// Visits all subschemas of the [`SchemaObject`]. -pub fn visit_schema_object(v: &mut V, schema: &mut SchemaObject) { - if let Some(sub) = &mut schema.subschemas { - visit_vec(v, &mut sub.all_of); - visit_vec(v, &mut sub.any_of); - visit_vec(v, &mut sub.one_of); - visit_box(v, &mut sub.not); - visit_box(v, &mut sub.if_schema); - visit_box(v, &mut sub.then_schema); - visit_box(v, &mut sub.else_schema); - } - - if let Some(arr) = &mut schema.array { - visit_single_or_vec(v, &mut arr.items); - visit_box(v, &mut arr.additional_items); - visit_box(v, &mut arr.contains); - } - - if let Some(obj) = &mut schema.object { - visit_map_values(v, &mut obj.properties); - visit_map_values(v, &mut obj.pattern_properties); - visit_box(v, &mut obj.additional_properties); - visit_box(v, &mut obj.property_names); - } -} - -fn visit_box(v: &mut V, target: &mut Option>) { - if let Some(s) = target { - v.visit_schema(s) - } -} - -fn visit_vec(v: &mut V, target: &mut Option>) { - if let Some(vec) = target { - for s in vec { - v.visit_schema(s) - } - } -} - -fn visit_map_values(v: &mut V, target: &mut crate::Map) { - for s in target.values_mut() { - v.visit_schema(s) - } -} - -fn visit_single_or_vec(v: &mut V, target: &mut Option>) { - match target { - None => {} - Some(SingleOrVec::Single(s)) => v.visit_schema(s), - Some(SingleOrVec::Vec(vec)) => { - for s in vec { - v.visit_schema(s) - } - } - } -} - -/// This visitor will replace all boolean JSON Schemas with equivalent object schemas. -/// -/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as schemas. -#[derive(Debug, Clone)] -pub struct ReplaceBoolSchemas { - /// When set to `true`, a schema's `additionalProperties` property will not be changed from a boolean. - pub skip_additional_properties: bool, -} - -impl Visitor for ReplaceBoolSchemas { - fn visit_schema(&mut self, schema: &mut Schema) { - visit_schema(self, schema); - - if let Schema::Bool(b) = *schema { - *schema = Schema::Bool(b).into_object().into() - } - } - - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - if self.skip_additional_properties { - if let Some(obj) = &mut schema.object { - if let Some(ap) = &obj.additional_properties { - if let Schema::Bool(_) = ap.as_ref() { - let additional_properties = obj.additional_properties.take(); - visit_schema_object(self, schema); - schema.object().additional_properties = additional_properties; - - return; - } - } - } - } - - visit_schema_object(self, schema); - } -} - -/// This visitor will restructure JSON Schema objects so that the `$ref` property will never appear alongside any other properties. -/// -/// This is useful for dialects of JSON Schema (e.g. Draft 7) that do not support other properties alongside `$ref`. -#[derive(Debug, Clone)] -pub struct RemoveRefSiblings; - -impl Visitor for RemoveRefSiblings { - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - visit_schema_object(self, schema); - - if let Some(reference) = schema.reference.take() { - if schema == &SchemaObject::default() { - schema.reference = Some(reference); - } else { - let ref_schema = Schema::new_ref(reference); - let all_of = &mut schema.subschemas().all_of; - match all_of { - Some(vec) => vec.push(ref_schema), - None => *all_of = Some(vec![ref_schema]), - } - } - } - } -} - -/// This visitor will remove the `examples` schema property and (if present) set its first value as the `example` property. -/// -/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property. -#[derive(Debug, Clone)] -pub struct SetSingleExample { - /// When set to `true`, the `examples` property will not be removed, but its first value will still be copied to `example`. - pub retain_examples: bool, -} - -impl Visitor for SetSingleExample { - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - visit_schema_object(self, schema); - - let first_example = schema.metadata.as_mut().and_then(|m| { - if self.retain_examples { - m.examples.first().cloned() - } else { - m.examples.drain(..).next() - } - }); - - if let Some(example) = first_example { - schema.extensions.insert("example".to_owned(), example); - } - } -} diff --git a/schemars/tests/arrayvec.rs b/schemars/tests/arrayvec.rs index b00b3955..e32d1a72 100644 --- a/schemars/tests/arrayvec.rs +++ b/schemars/tests/arrayvec.rs @@ -1,16 +1,6 @@ mod util; use util::*; -#[test] -fn arrayvec05() -> TestResult { - test_default_generated_schema::>("arrayvec") -} - -#[test] -fn arrayvec05_string() -> TestResult { - test_default_generated_schema::>("arrayvec_string") -} - #[test] fn arrayvec07() -> TestResult { test_default_generated_schema::>("arrayvec") diff --git a/schemars/tests/bytes.rs b/schemars/tests/bytes.rs index 688ab214..57974117 100644 --- a/schemars/tests/bytes.rs +++ b/schemars/tests/bytes.rs @@ -1,5 +1,5 @@ mod util; -use bytes::{Bytes, BytesMut}; +use bytes1::{Bytes, BytesMut}; use util::*; #[test] diff --git a/schemars/tests/chrono.rs b/schemars/tests/chrono.rs index 7f518d79..da6f8d4d 100644 --- a/schemars/tests/chrono.rs +++ b/schemars/tests/chrono.rs @@ -1,5 +1,5 @@ mod util; -use chrono::prelude::*; +use chrono04::prelude::*; use schemars::JsonSchema; use util::*; diff --git a/schemars/tests/decimal.rs b/schemars/tests/decimal.rs new file mode 100644 index 00000000..246e8138 --- /dev/null +++ b/schemars/tests/decimal.rs @@ -0,0 +1,12 @@ +mod util; +use util::*; + +#[test] +fn rust_decimal() -> TestResult { + test_default_generated_schema::("rust_decimal") +} + +#[test] +fn bigdecimal04() -> TestResult { + test_default_generated_schema::("bigdecimal04") +} diff --git a/schemars/tests/default.rs b/schemars/tests/default.rs index fbc1033b..ab489f5e 100644 --- a/schemars/tests/default.rs +++ b/schemars/tests/default.rs @@ -30,6 +30,7 @@ where struct MyStruct { my_int: i32, my_bool: bool, + my_optional_string: Option, #[serde(serialize_with = "custom_serialize")] my_struct2: MyStruct2, #[serde( diff --git a/schemars/tests/dereference.rs b/schemars/tests/dereference.rs deleted file mode 100644 index b1abab0f..00000000 --- a/schemars/tests/dereference.rs +++ /dev/null @@ -1,23 +0,0 @@ -use schemars::{gen::SchemaGenerator, JsonSchema}; -use std::ptr; - -#[allow(dead_code)] -#[derive(JsonSchema)] -struct Struct { - foo: i32, - bar: bool, -} - -#[test] -fn dereference_struct() { - let mut gen = SchemaGenerator::default(); - let struct_ref_schema = gen.subschema_for::(); - let struct_schema = gen.definitions().get(&::schema_name()).unwrap(); - - assert!(struct_ref_schema.is_ref()); - assert!(!struct_schema.is_ref()); - - let dereferenced = gen.dereference(&struct_ref_schema); - assert!(dereferenced.is_some()); - assert!(ptr::eq(dereferenced.unwrap(), struct_schema)); -} diff --git a/schemars/tests/docs.rs b/schemars/tests/docs.rs index 788140d8..7086c693 100644 --- a/schemars/tests/docs.rs +++ b/schemars/tests/docs.rs @@ -1,26 +1,26 @@ mod util; -use schemars::{gen::SchemaSettings, JsonSchema}; +use schemars::JsonSchema; use util::*; #[allow(dead_code)] #[derive(JsonSchema)] /** - * - * # This is the struct's title - * - * This is the struct's description. - * - */ +# This is the struct's title + +This is the struct's description. +*/ struct MyStruct { /// # An integer my_int: i32, my_undocumented_bool: bool, /// A unit struct instance my_unit: MyUnitStruct, + #[doc = concat!("# Documented ", "bool")] + #[doc = concat!("This bool is documented")] + my_documented_bool: bool, } /// # A Unit -/// #[derive(JsonSchema)] struct MyUnitStruct; @@ -59,12 +59,6 @@ fn doc_comments_struct() -> TestResult { test_default_generated_schema::("doc_comments_struct") } -#[test] -fn doc_comments_struct_ref_siblings() -> TestResult { - let settings = SchemaSettings::draft2019_09(); - test_generated_schema::("doc_comments_struct_ref_siblings", settings) -} - #[test] fn doc_comments_enum() -> TestResult { test_default_generated_schema::("doc_comments_enum") @@ -83,6 +77,8 @@ struct OverrideDocs { /// Also overridden #[schemars(title = "", description = "")] my_undocumented_bool: bool, + #[schemars(title = concat!("Documented ", "bool"), description = "Capitalized".to_uppercase())] + my_documented_bool: bool, } #[test] diff --git a/schemars/tests/either.rs b/schemars/tests/either.rs index 16a2e591..5dcd0796 100644 --- a/schemars/tests/either.rs +++ b/schemars/tests/either.rs @@ -1,5 +1,5 @@ mod util; -use either::Either; +use either1::Either; use util::*; #[test] diff --git a/schemars/tests/enum.rs b/schemars/tests/enum.rs index 3f60e843..45698a78 100644 --- a/schemars/tests/enum.rs +++ b/schemars/tests/enum.rs @@ -1,5 +1,7 @@ mod util; -use schemars::{JsonSchema, Map}; +use std::collections::BTreeMap; + +use schemars::JsonSchema; use util::*; // Ensure that schemars_derive uses the full path to std::string::String @@ -20,7 +22,7 @@ struct Struct { #[schemars(rename_all = "camelCase")] enum External { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -43,7 +45,7 @@ fn enum_external_tag() -> TestResult { #[schemars(tag = "typeProperty")] enum Internal { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -65,7 +67,7 @@ fn enum_internal_tag() -> TestResult { #[schemars(untagged)] enum Untagged { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -87,7 +89,7 @@ fn enum_untagged() -> TestResult { #[schemars(tag = "t", content = "c")] enum Adjacent { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -136,3 +138,48 @@ enum SoundOfMusic { fn enum_unit_with_doc_comments() -> TestResult { test_default_generated_schema::("enum-unit-doc") } + +#[derive(JsonSchema)] +enum NoVariants {} + +#[test] +fn enum_no_variants() -> TestResult { + test_default_generated_schema::("no-variants") +} + +#[derive(JsonSchema)] +#[serde(rename_all_fields = "PascalCase")] +pub enum RenameAllFields { + First { + nested_attribute: std::string::String, + }, +} + +#[derive(JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RenameAll { + First { nested_attribute: bool }, +} + +#[derive(JsonSchema)] +pub enum RenameAttribute { + First { + #[serde(rename = "RenamedAttribute")] + nested_attribute: std::string::String, + }, +} + +#[test] +fn enum_unit_rename_attribute() -> TestResult { + test_default_generated_schema::("enum-rename-attr") +} + +#[test] +fn enum_unit_rename_all_fields() -> TestResult { + test_default_generated_schema::("enum-rename-all-fields") +} + +#[test] +fn enum_unit_rename_all() -> TestResult { + test_default_generated_schema::("enum-rename-all") +} diff --git a/schemars/tests/enum_deny_unknown_fields.rs b/schemars/tests/enum_deny_unknown_fields.rs index 62017840..ef56d058 100644 --- a/schemars/tests/enum_deny_unknown_fields.rs +++ b/schemars/tests/enum_deny_unknown_fields.rs @@ -1,5 +1,7 @@ mod util; -use schemars::{JsonSchema, Map}; +use std::collections::BTreeMap; + +use schemars::JsonSchema; use util::*; // Ensure that schemars_derive uses the full path to std::string::String @@ -22,7 +24,7 @@ struct Struct { #[schemars(rename_all = "camelCase", deny_unknown_fields)] enum External { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -46,7 +48,7 @@ fn enum_external_tag() -> TestResult { #[schemars(tag = "typeProperty", deny_unknown_fields)] enum Internal { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -69,7 +71,7 @@ fn enum_internal_tag() -> TestResult { #[schemars(untagged, deny_unknown_fields)] enum Untagged { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -92,7 +94,7 @@ fn enum_untagged() -> TestResult { #[schemars(tag = "t", content = "c", deny_unknown_fields)] enum Adjacent { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { diff --git a/schemars/tests/enum_flatten.rs b/schemars/tests/enum_flatten.rs new file mode 100644 index 00000000..4c94a4c5 --- /dev/null +++ b/schemars/tests/enum_flatten.rs @@ -0,0 +1,89 @@ +mod util; +use schemars::{generate::SchemaSettings, JsonSchema}; +use util::*; + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct Flat { + f: f32, + #[schemars(flatten)] + e1: Enum1, + #[schemars(flatten)] + e2: Enum2, + #[schemars(flatten)] + e3: Enum3, + #[schemars(flatten)] + e4: Enum4, + #[schemars(flatten)] + e5: Enum5, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +enum Enum1 { + B(bool), + S(String), +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +enum Enum2 { + U(u32), + F(f64), +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +enum Enum3 { + B2(bool), + S2(String), +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +enum Enum4 { + U2(u32), + F2(f64), +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +enum Enum5 { + B3(bool), + S3(String), +} + +#[test] +fn test_flat_schema() -> TestResult { + test_default_generated_schema::("enum_flatten") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(deny_unknown_fields)] +struct FlatDenyUnknownFields { + f: f32, + #[schemars(flatten)] + e1: Enum1, + #[schemars(flatten)] + e2: Enum2, + #[schemars(flatten)] + e3: Enum3, + #[schemars(flatten)] + e4: Enum4, + #[schemars(flatten)] + e5: Enum5, +} + +#[test] +fn test_flat_schema_duf() -> TestResult { + test_default_generated_schema::("enum_flatten_duf") +} + +#[test] +fn test_flat_schema_duf_draft07() -> TestResult { + test_generated_schema::( + "enum_flatten_duf_draft07", + SchemaSettings::draft07(), + ) +} diff --git a/schemars/tests/enumset.rs b/schemars/tests/enumset.rs index 27f20307..c04ba0e1 100644 --- a/schemars/tests/enumset.rs +++ b/schemars/tests/enumset.rs @@ -1,8 +1,11 @@ mod util; -use enumset::{EnumSet, EnumSetType}; +use enumset1::{EnumSet, EnumSetType}; use schemars::JsonSchema; use util::*; +// needed to derive EnumSetType when using a crate alias +extern crate enumset1 as enumset; + #[derive(EnumSetType, JsonSchema)] enum Foo { Bar, diff --git a/schemars/tests/expected/arrayvec.json b/schemars/tests/expected/arrayvec.json index 3de09a08..998d0872 100644 --- a/schemars/tests/expected/arrayvec.json +++ b/schemars/tests/expected/arrayvec.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Array_up_to_size_16_of_int32", "type": "array", "items": { diff --git a/schemars/tests/expected/arrayvec_string.json b/schemars/tests/expected/arrayvec_string.json index 42f099a3..0604332c 100644 --- a/schemars/tests/expected/arrayvec_string.json +++ b/schemars/tests/expected/arrayvec_string.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "String", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "string", "type": "string" } \ 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..e94ca271 --- /dev/null +++ b/schemars/tests/expected/bigdecimal04.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Decimal", + "type": "string", + "pattern": "^-?[0-9]+(\\.[0-9]+)?$" +} \ No newline at end of file diff --git a/schemars/tests/expected/bound.json b/schemars/tests/expected/bound.json index 3c022dcf..a5645c67 100644 --- a/schemars/tests/expected/bound.json +++ b/schemars/tests/expected/bound.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyContainer", "type": "object", - "required": [ - "associated", - "generic" - ], "properties": { "associated": { "type": "string" @@ -13,5 +9,9 @@ "generic": { "type": "null" } - } + }, + "required": [ + "associated", + "generic" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/bytes.json b/schemars/tests/expected/bytes.json index 5e1f9a54..80486c13 100644 --- a/schemars/tests/expected/bytes.json +++ b/schemars/tests/expected/bytes.json @@ -1,14 +1,14 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Tuple_of_Array_of_uint8_and_Array_of_uint8", "type": "array", - "items": [ + "prefixItems": [ { "type": "array", "items": { "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0 } }, { @@ -16,10 +16,10 @@ "items": { "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0 } } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } \ No newline at end of file diff --git a/schemars/tests/expected/chrono-types.json b/schemars/tests/expected/chrono-types.json index e3e788f7..de261307 100644 --- a/schemars/tests/expected/chrono-types.json +++ b/schemars/tests/expected/chrono-types.json @@ -1,14 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "ChronoTypes", "type": "object", - "required": [ - "date_time", - "naive_date", - "naive_date_time", - "naive_time", - "weekday" - ], "properties": { "weekday": { "type": "string", @@ -38,5 +31,12 @@ "type": "string", "format": "partial-date-time" } - } + }, + "required": [ + "weekday", + "date_time", + "naive_date", + "naive_date_time", + "naive_time" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/crate_alias.json b/schemars/tests/expected/crate_alias.json index 66bf749b..c2f14305 100644 --- a/schemars/tests/expected/crate_alias.json +++ b/schemars/tests/expected/crate_alias.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "description": "This is a document", @@ -15,5 +11,9 @@ "bar": { "type": "boolean" } - } + }, + "required": [ + "foo", + "bar" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/default.json b/schemars/tests/expected/default.json index aefef83d..72e580d4 100644 --- a/schemars/tests/expected/default.json +++ b/schemars/tests/expected/default.json @@ -1,44 +1,47 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", "properties": { "my_int": { - "default": 0, "type": "integer", - "format": "int32" + "format": "int32", + "default": 0 }, "my_bool": { - "default": false, - "type": "boolean" + "type": "boolean", + "default": false + }, + "my_optional_string": { + "type": [ + "string", + "null" + ], + "default": null }, "my_struct2": { - "default": "i:0 b:false", - "allOf": [ - { - "$ref": "#/definitions/MyStruct2" - } - ] + "$ref": "#/$defs/MyStruct2", + "default": "i:0 b:false" }, "my_struct2_default_skipped": { - "$ref": "#/definitions/MyStruct2" + "$ref": "#/$defs/MyStruct2" }, "not_serialize": { - "$ref": "#/definitions/NotSerialize" + "$ref": "#/$defs/NotSerialize" } }, - "definitions": { + "$defs": { "MyStruct2": { "type": "object", "properties": { "my_int": { - "default": 6, "type": "integer", - "format": "int32" + "format": "int32", + "default": 6 }, "my_bool": { - "default": true, - "type": "boolean" + "type": "boolean", + "default": true } } }, diff --git a/schemars/tests/expected/deprecated-enum.json b/schemars/tests/expected/deprecated-enum.json index 825ae50d..a61c3223 100644 --- a/schemars/tests/expected/deprecated-enum.json +++ b/schemars/tests/expected/deprecated-enum.json @@ -1,7 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DeprecatedEnum", - "deprecated": true, "oneOf": [ { "type": "string", @@ -10,38 +9,37 @@ ] }, { - "deprecated": true, "type": "string", - "enum": [ - "DeprecatedUnitVariant" - ] + "const": "DeprecatedUnitVariant", + "deprecated": true }, { - "deprecated": true, "type": "object", - "required": [ - "DeprecatedStructVariant" - ], "properties": { "DeprecatedStructVariant": { "type": "object", - "required": [ - "deprecated_field", - "foo" - ], "properties": { - "deprecated_field": { - "deprecated": true, - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "deprecated_field": { + "type": "boolean", + "deprecated": true } - } + }, + "required": [ + "foo", + "deprecated_field" + ] } }, - "additionalProperties": false + "required": [ + "DeprecatedStructVariant" + ], + "additionalProperties": false, + "deprecated": true } - ] + ], + "deprecated": true } \ No newline at end of file diff --git a/schemars/tests/expected/deprecated-struct.json b/schemars/tests/expected/deprecated-struct.json index 8f7ba711..b7396ab0 100644 --- a/schemars/tests/expected/deprecated-struct.json +++ b/schemars/tests/expected/deprecated-struct.json @@ -1,20 +1,20 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DeprecatedStruct", - "deprecated": true, "type": "object", - "required": [ - "deprecated_field", - "foo" - ], "properties": { "foo": { "type": "integer", "format": "int32" }, "deprecated_field": { - "deprecated": true, - "type": "boolean" + "type": "boolean", + "deprecated": true } - } + }, + "required": [ + "foo", + "deprecated_field" + ], + "deprecated": true } \ No newline at end of file diff --git a/schemars/tests/expected/doc_comments_enum.json b/schemars/tests/expected/doc_comments_enum.json index 53aaa86b..0da3bed2 100644 --- a/schemars/tests/expected/doc_comments_enum.json +++ b/schemars/tests/expected/doc_comments_enum.json @@ -1,7 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "This is the enum's title", - "description": "This is the enum's description.", + "description": "This is \n the enum's description.", "oneOf": [ { "type": "string", @@ -13,24 +13,19 @@ { "description": "This comment is included in the generated schema :)", "type": "string", - "enum": [ - "DocumentedUnit" - ] + "const": "DocumentedUnit" }, { "title": "Complex variant", "description": "This is a struct-like variant.", "type": "object", - "required": [ - "Complex" - ], "properties": { "Complex": { "type": "object", "properties": { "my_nullable_string": { "title": "A nullable string", - "description": "This field is a nullable string.\n\nThis is the second line!\n\nAnd this is the third!", + "description": "This field is a nullable string.\n\n This\nis\n the second\n line!\n\n\n\n\n And this is the third!", "type": [ "string", "null" @@ -39,6 +34,9 @@ } } }, + "required": [ + "Complex" + ], "additionalProperties": false } ] diff --git a/schemars/tests/expected/doc_comments_override.json b/schemars/tests/expected/doc_comments_override.json index 83dce791..00eebe57 100644 --- a/schemars/tests/expected/doc_comments_override.json +++ b/schemars/tests/expected/doc_comments_override.json @@ -1,12 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "OverrideDocs struct", "description": "New description", "type": "object", - "required": [ - "my_int", - "my_undocumented_bool" - ], "properties": { "my_int": { "title": "My integer", @@ -16,6 +12,16 @@ }, "my_undocumented_bool": { "type": "boolean" + }, + "my_documented_bool": { + "title": "Documented bool", + "description": "CAPITALIZED", + "type": "boolean" } - } + }, + "required": [ + "my_int", + "my_undocumented_bool", + "my_documented_bool" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/doc_comments_struct.json b/schemars/tests/expected/doc_comments_struct.json index 9f1b4bfa..670a4d26 100644 --- a/schemars/tests/expected/doc_comments_struct.json +++ b/schemars/tests/expected/doc_comments_struct.json @@ -1,13 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "This is the struct's title", "description": "This is the struct's description.", "type": "object", - "required": [ - "my_int", - "my_undocumented_bool", - "my_unit" - ], "properties": { "my_int": { "title": "An integer", @@ -19,14 +14,21 @@ }, "my_unit": { "description": "A unit struct instance", - "allOf": [ - { - "$ref": "#/definitions/MyUnitStruct" - } - ] + "$ref": "#/$defs/MyUnitStruct" + }, + "my_documented_bool": { + "title": "Documented bool", + "description": "This bool is documented", + "type": "boolean" } }, - "definitions": { + "required": [ + "my_int", + "my_undocumented_bool", + "my_unit", + "my_documented_bool" + ], + "$defs": { "MyUnitStruct": { "title": "A Unit", "type": "null" diff --git a/schemars/tests/expected/doc_comments_struct_ref_siblings.json b/schemars/tests/expected/doc_comments_struct_ref_siblings.json deleted file mode 100644 index 35bb648e..00000000 --- a/schemars/tests/expected/doc_comments_struct_ref_siblings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "This is the struct's title", - "description": "This is the struct's description.", - "type": "object", - "required": [ - "my_int", - "my_undocumented_bool", - "my_unit" - ], - "properties": { - "my_int": { - "title": "An integer", - "type": "integer", - "format": "int32" - }, - "my_undocumented_bool": { - "type": "boolean" - }, - "my_unit": { - "description": "A unit struct instance", - "$ref": "#/definitions/MyUnitStruct" - } - }, - "definitions": { - "MyUnitStruct": { - "title": "A Unit", - "type": "null" - } - } -} \ No newline at end of file diff --git a/schemars/tests/expected/duration_and_systemtime.json b/schemars/tests/expected/duration_and_systemtime.json index d01f7d56..7e301e5b 100644 --- a/schemars/tests/expected/duration_and_systemtime.json +++ b/schemars/tests/expected/duration_and_systemtime.json @@ -1,57 +1,57 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "duration", - "time" - ], "properties": { "duration": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" }, "time": { - "$ref": "#/definitions/SystemTime" + "$ref": "#/$defs/SystemTime" } }, - "definitions": { + "required": [ + "duration", + "time" + ], + "$defs": { "Duration": { "type": "object", - "required": [ - "nanos", - "secs" - ], "properties": { "secs": { "type": "integer", "format": "uint64", - "minimum": 0.0 + "minimum": 0 }, "nanos": { "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0 } - } + }, + "required": [ + "secs", + "nanos" + ] }, "SystemTime": { "type": "object", - "required": [ - "nanos_since_epoch", - "secs_since_epoch" - ], "properties": { "secs_since_epoch": { "type": "integer", "format": "uint64", - "minimum": 0.0 + "minimum": 0 }, "nanos_since_epoch": { "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0 } - } + }, + "required": [ + "secs_since_epoch", + "nanos_since_epoch" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/either.json b/schemars/tests/expected/either.json index b028057a..18d8f517 100644 --- a/schemars/tests/expected/either.json +++ b/schemars/tests/expected/either.json @@ -1,6 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Either_int32_or_Either_Boolean_or_Null", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Either_int32_or_Either_boolean_or_null", "anyOf": [ { "type": "integer", diff --git a/schemars/tests/expected/enum-adjacent-tagged-duf.json b/schemars/tests/expected/enum-adjacent-tagged-duf.json index c5b54c81..dfb3bb8b 100644 --- a/schemars/tests/expected/enum-adjacent-tagged-duf.json +++ b/schemars/tests/expected/enum-adjacent-tagged-duf.json @@ -1,12 +1,9 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Adjacent", "oneOf": [ { "type": "object", - "required": [ - "t" - ], "properties": { "t": { "type": "string", @@ -15,111 +12,116 @@ ] } }, + "required": [ + "t" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "t": { "type": "string", "enum": [ "StringMap" ] + }, + "c": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, + "required": [ + "t", + "c" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "$ref": "#/definitions/UnitStruct" - }, "t": { "type": "string", "enum": [ "UnitStructNewType" ] + }, + "c": { + "$ref": "#/$defs/UnitStruct" } }, + "required": [ + "t", + "c" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "$ref": "#/definitions/Struct" - }, "t": { "type": "string", "enum": [ "StructNewType" ] + }, + "c": { + "$ref": "#/$defs/Struct" } }, + "required": [ + "t", + "c" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Struct" + ] + }, "c": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } }, - "additionalProperties": false - }, - "t": { - "type": "string", - "enum": [ - "Struct" + "additionalProperties": false, + "required": [ + "foo", + "bar" ] } }, + "required": [ + "t", + "c" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + }, "c": { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -128,23 +130,18 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 - }, - "t": { - "type": "string", - "enum": [ - "Tuple" - ] + "minItems": 2, + "maxItems": 2 } }, + "required": [ + "t", + "c" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "t" - ], "properties": { "t": { "type": "string", @@ -153,48 +150,51 @@ ] } }, + "required": [ + "t" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "integer", - "format": "int32" - }, "t": { "type": "string", "enum": [ "WithInt" ] + }, + "c": { + "type": "integer", + "format": "int32" } }, + "required": [ + "t", + "c" + ], "additionalProperties": false } ], - "definitions": { + "$defs": { + "UnitStruct": { + "type": "null" + }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } - }, - "UnitStruct": { - "type": "null" + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum-adjacent-tagged.json b/schemars/tests/expected/enum-adjacent-tagged.json index efe19dc8..c631ae54 100644 --- a/schemars/tests/expected/enum-adjacent-tagged.json +++ b/schemars/tests/expected/enum-adjacent-tagged.json @@ -1,12 +1,9 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Adjacent", "oneOf": [ { "type": "object", - "required": [ - "t" - ], "properties": { "t": { "type": "string", @@ -14,106 +11,111 @@ "UnitOne" ] } - } + }, + "required": [ + "t" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "t": { "type": "string", "enum": [ "StringMap" ] + }, + "c": { + "type": "object", + "additionalProperties": { + "type": "string" + } } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "$ref": "#/definitions/UnitStruct" - }, "t": { "type": "string", "enum": [ "UnitStructNewType" ] + }, + "c": { + "$ref": "#/$defs/UnitStruct" } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "$ref": "#/definitions/Struct" - }, "t": { "type": "string", "enum": [ "StructNewType" ] + }, + "c": { + "$ref": "#/$defs/Struct" } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Struct" + ] + }, "c": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } - }, - "t": { - "type": "string", - "enum": [ - "Struct" + }, + "required": [ + "foo", + "bar" ] } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + }, "c": { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -122,22 +124,17 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 - }, - "t": { - "type": "string", - "enum": [ - "Tuple" - ] + "minItems": 2, + "maxItems": 2 } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "t" - ], "properties": { "t": { "type": "string", @@ -145,47 +142,50 @@ "UnitTwo" ] } - } + }, + "required": [ + "t" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "integer", - "format": "int32" - }, "t": { "type": "string", "enum": [ "WithInt" ] + }, + "c": { + "type": "integer", + "format": "int32" } - } + }, + "required": [ + "t", + "c" + ] } ], - "definitions": { + "$defs": { + "UnitStruct": { + "type": "null" + }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } - }, - "UnitStruct": { - "type": "null" + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum-external-duf.json b/schemars/tests/expected/enum-external-duf.json index b6b7b99a..76be5b3f 100644 --- a/schemars/tests/expected/enum-external-duf.json +++ b/schemars/tests/expected/enum-external-duf.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "External", "oneOf": [ { @@ -11,9 +11,6 @@ }, { "type": "object", - "required": [ - "stringMap" - ], "properties": { "stringMap": { "type": "object", @@ -22,67 +19,67 @@ } } }, + "required": [ + "stringMap" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "unitStructNewType" - ], "properties": { "unitStructNewType": { - "$ref": "#/definitions/UnitStruct" + "$ref": "#/$defs/UnitStruct" } }, + "required": [ + "unitStructNewType" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "structNewType" - ], "properties": { "structNewType": { - "$ref": "#/definitions/Struct" + "$ref": "#/$defs/Struct" } }, + "required": [ + "structNewType" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "struct" - ], "properties": { "struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "foo", + "bar" + ] } }, + "required": [ + "struct" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "tuple" - ], "properties": { "tuple": { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -91,45 +88,48 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } }, + "required": [ + "tuple" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "withInt" - ], "properties": { "withInt": { "type": "integer", "format": "int32" } }, + "required": [ + "withInt" + ], "additionalProperties": false } ], - "definitions": { + "$defs": { + "UnitStruct": { + "type": "null" + }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } - }, - "UnitStruct": { - "type": "null" + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum-external.json b/schemars/tests/expected/enum-external.json index cc721dfd..3c660fb9 100644 --- a/schemars/tests/expected/enum-external.json +++ b/schemars/tests/expected/enum-external.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "External", "oneOf": [ { @@ -11,9 +11,6 @@ }, { "type": "object", - "required": [ - "stringMap" - ], "properties": { "stringMap": { "type": "object", @@ -22,66 +19,66 @@ } } }, + "required": [ + "stringMap" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "unitStructNewType" - ], "properties": { "unitStructNewType": { - "$ref": "#/definitions/UnitStruct" + "$ref": "#/$defs/UnitStruct" } }, + "required": [ + "unitStructNewType" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "structNewType" - ], "properties": { "structNewType": { - "$ref": "#/definitions/Struct" + "$ref": "#/$defs/Struct" } }, + "required": [ + "structNewType" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "struct" - ], "properties": { "struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } + }, + "required": [ + "foo", + "bar" + ] } }, + "required": [ + "struct" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "tuple" - ], "properties": { "tuple": { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -90,45 +87,48 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } }, + "required": [ + "tuple" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "withInt" - ], "properties": { "withInt": { "type": "integer", "format": "int32" } }, + "required": [ + "withInt" + ], "additionalProperties": false } ], - "definitions": { + "$defs": { + "UnitStruct": { + "type": "null" + }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" + }, + "bar": { + "type": "boolean" } - } - }, - "UnitStruct": { - "type": "null" + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum-internal-duf.json b/schemars/tests/expected/enum-internal-duf.json index fc36644f..73e4743b 100644 --- a/schemars/tests/expected/enum-internal-duf.json +++ b/schemars/tests/expected/enum-internal-duf.json @@ -1,134 +1,116 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Internal", "oneOf": [ { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitOne" - ] + "const": "UnitOne" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "StringMap" - ] + "const": "StringMap" } }, "additionalProperties": { "type": "string" - } + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitStructNewType" - ] + "const": "UnitStructNewType" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "bar", - "foo", - "typeProperty" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" }, + "bar": { + "type": "boolean" + }, "typeProperty": { "type": "string", - "enum": [ - "StructNewType" - ] + "const": "StructNewType" } - } + }, + "required": [ + "typeProperty", + "foo", + "bar" + ] }, { "type": "object", - "required": [ - "bar", - "foo", - "typeProperty" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" }, + "bar": { + "type": "boolean" + }, "typeProperty": { "type": "string", - "enum": [ - "Struct" - ] + "const": "Struct" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "typeProperty", + "foo", + "bar" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitTwo" - ] + "const": "UnitTwo" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false }, { - "type": [ - "object", - "integer" - ], + "type": "object", "format": "int32", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "WithInt" - ] + "const": "WithInt" } }, - "additionalProperties": false + "required": [ + "typeProperty" + ] } ] } \ No newline at end of file diff --git a/schemars/tests/expected/enum-internal.json b/schemars/tests/expected/enum-internal.json index 37739b09..2fd9770f 100644 --- a/schemars/tests/expected/enum-internal.json +++ b/schemars/tests/expected/enum-internal.json @@ -1,126 +1,112 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Internal", "oneOf": [ { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitOne" - ] + "const": "UnitOne" } - } + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "StringMap" - ] + "const": "StringMap" } - } + }, + "additionalProperties": { + "type": "string" + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitStructNewType" - ] + "const": "UnitStructNewType" } - } + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "bar", - "foo", - "typeProperty" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" }, + "bar": { + "type": "boolean" + }, "typeProperty": { "type": "string", - "enum": [ - "StructNewType" - ] + "const": "StructNewType" } - } + }, + "required": [ + "typeProperty", + "foo", + "bar" + ] }, { "type": "object", - "required": [ - "bar", - "foo", - "typeProperty" - ], "properties": { - "bar": { - "type": "boolean" - }, "foo": { "type": "integer", "format": "int32" }, + "bar": { + "type": "boolean" + }, "typeProperty": { "type": "string", - "enum": [ - "Struct" - ] + "const": "Struct" } - } + }, + "required": [ + "typeProperty", + "foo", + "bar" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "UnitTwo" - ] + "const": "UnitTwo" } - } + }, + "required": [ + "typeProperty" + ] }, { - "type": [ - "object", - "integer" - ], + "type": "object", "format": "int32", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "WithInt" - ] + "const": "WithInt" } - } + }, + "required": [ + "typeProperty" + ] } ] } \ No newline at end of file diff --git a/schemars/tests/expected/enum-rename-all-fields.json b/schemars/tests/expected/enum-rename-all-fields.json new file mode 100644 index 00000000..422a6944 --- /dev/null +++ b/schemars/tests/expected/enum-rename-all-fields.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RenameAllFields", + "oneOf": [ + { + "type": "object", + "properties": { + "First": { + "type": "object", + "properties": { + "NestedAttribute": { + "type": "string" + } + }, + "required": [ + "NestedAttribute" + ] + } + }, + "required": [ + "First" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum-rename-all.json b/schemars/tests/expected/enum-rename-all.json new file mode 100644 index 00000000..eb237be4 --- /dev/null +++ b/schemars/tests/expected/enum-rename-all.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RenameAll", + "oneOf": [ + { + "type": "object", + "properties": { + "first": { + "type": "object", + "properties": { + "nested_attribute": { + "type": "boolean" + } + }, + "required": [ + "nested_attribute" + ] + } + }, + "required": [ + "first" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum-rename-attr.json b/schemars/tests/expected/enum-rename-attr.json new file mode 100644 index 00000000..a9f3706a --- /dev/null +++ b/schemars/tests/expected/enum-rename-attr.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RenameAttribute", + "oneOf": [ + { + "type": "object", + "properties": { + "First": { + "type": "object", + "properties": { + "RenamedAttribute": { + "type": "string" + } + }, + "required": [ + "RenamedAttribute" + ] + } + }, + "required": [ + "First" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum-repr-with-attrs.json b/schemars/tests/expected/enum-repr-with-attrs.json index 7070de82..89d941b7 100644 --- a/schemars/tests/expected/enum-repr-with-attrs.json +++ b/schemars/tests/expected/enum-repr-with-attrs.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Renamed", "description": "Description from comment", "type": "integer", diff --git a/schemars/tests/expected/enum-repr.json b/schemars/tests/expected/enum-repr.json index 92d6f3af..b3e6bb96 100644 --- a/schemars/tests/expected/enum-repr.json +++ b/schemars/tests/expected/enum-repr.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Enum", "type": "integer", "enum": [ diff --git a/schemars/tests/expected/enum-simple-internal-duf.json b/schemars/tests/expected/enum-simple-internal-duf.json index 833f7b73..aaf1eef8 100644 --- a/schemars/tests/expected/enum-simple-internal-duf.json +++ b/schemars/tests/expected/enum-simple-internal-duf.json @@ -1,50 +1,44 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "SimpleInternal", "oneOf": [ { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "A" - ] + "const": "A" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "B" - ] + "const": "B" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "C" - ] + "const": "C" } }, + "required": [ + "typeProperty" + ], "additionalProperties": false } ] diff --git a/schemars/tests/expected/enum-simple-internal.json b/schemars/tests/expected/enum-simple-internal.json index 50cd62c1..e955d2af 100644 --- a/schemars/tests/expected/enum-simple-internal.json +++ b/schemars/tests/expected/enum-simple-internal.json @@ -1,48 +1,42 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "SimpleInternal", "oneOf": [ { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "A" - ] + "const": "A" } - } + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "B" - ] + "const": "B" } - } + }, + "required": [ + "typeProperty" + ] }, { "type": "object", - "required": [ - "typeProperty" - ], "properties": { "typeProperty": { "type": "string", - "enum": [ - "C" - ] + "const": "C" } - } + }, + "required": [ + "typeProperty" + ] } ] } \ No newline at end of file diff --git a/schemars/tests/expected/enum-unit-doc.json b/schemars/tests/expected/enum-unit-doc.json index 11a5c2a5..10004354 100644 --- a/schemars/tests/expected/enum-unit-doc.json +++ b/schemars/tests/expected/enum-unit-doc.json @@ -1,28 +1,22 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "SoundOfMusic", "oneOf": [ { "title": "A deer", "description": "A female deer", "type": "string", - "enum": [ - "Do" - ] + "const": "Do" }, { "description": "A drop of golden sun", "type": "string", - "enum": [ - "Re" - ] + "const": "Re" }, { "description": "A name I call myself", "type": "string", - "enum": [ - "Mi" - ] + "const": "Mi" } ] } \ No newline at end of file diff --git a/schemars/tests/expected/enum-untagged-duf.json b/schemars/tests/expected/enum-untagged-duf.json index 24397bf2..58bdbe16 100644 --- a/schemars/tests/expected/enum-untagged-duf.json +++ b/schemars/tests/expected/enum-untagged-duf.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Untagged", "anyOf": [ { @@ -12,17 +12,13 @@ } }, { - "$ref": "#/definitions/UnitStruct" + "$ref": "#/$defs/UnitStruct" }, { - "$ref": "#/definitions/Struct" + "$ref": "#/$defs/Struct" }, { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -32,11 +28,15 @@ "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "foo", + "bar" + ] }, { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -45,24 +45,20 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 }, { "type": "integer", "format": "int32" } ], - "definitions": { + "$defs": { "UnitStruct": { "type": "null" }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -71,7 +67,11 @@ "bar": { "type": "boolean" } - } + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum-untagged.json b/schemars/tests/expected/enum-untagged.json index 7eccbe28..643cd20c 100644 --- a/schemars/tests/expected/enum-untagged.json +++ b/schemars/tests/expected/enum-untagged.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Untagged", "anyOf": [ { @@ -12,17 +12,13 @@ } }, { - "$ref": "#/definitions/UnitStruct" + "$ref": "#/$defs/UnitStruct" }, { - "$ref": "#/definitions/Struct" + "$ref": "#/$defs/Struct" }, { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -31,11 +27,15 @@ "bar": { "type": "boolean" } - } + }, + "required": [ + "foo", + "bar" + ] }, { "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -44,24 +44,20 @@ "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 }, { "type": "integer", "format": "int32" } ], - "definitions": { + "$defs": { "UnitStruct": { "type": "null" }, "Struct": { "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -70,7 +66,11 @@ "bar": { "type": "boolean" } - } + }, + "required": [ + "foo", + "bar" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/enum_flatten.json b/schemars/tests/expected/enum_flatten.json new file mode 100644 index 00000000..dffd89e1 --- /dev/null +++ b/schemars/tests/expected/enum_flatten.json @@ -0,0 +1,150 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Flat", + "type": "object", + "properties": { + "f": { + "type": "number", + "format": "float" + } + }, + "required": [ + "f" + ], + "allOf": [ + { + "oneOf": [ + { + "type": "object", + "properties": { + "B": { + "type": "boolean" + } + }, + "required": [ + "B" + ] + }, + { + "type": "object", + "properties": { + "S": { + "type": "string" + } + }, + "required": [ + "S" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U" + ] + }, + { + "type": "object", + "properties": { + "F": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "B2": { + "type": "boolean" + } + }, + "required": [ + "B2" + ] + }, + { + "type": "object", + "properties": { + "S2": { + "type": "string" + } + }, + "required": [ + "S2" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U2": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U2" + ] + }, + { + "type": "object", + "properties": { + "F2": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F2" + ] + } + ] + } + ], + "oneOf": [ + { + "type": "object", + "properties": { + "B3": { + "type": "boolean" + } + }, + "required": [ + "B3" + ] + }, + { + "type": "object", + "properties": { + "S3": { + "type": "string" + } + }, + "required": [ + "S3" + ] + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum_flatten_duf.json b/schemars/tests/expected/enum_flatten_duf.json new file mode 100644 index 00000000..37493cc5 --- /dev/null +++ b/schemars/tests/expected/enum_flatten_duf.json @@ -0,0 +1,151 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "FlatDenyUnknownFields", + "type": "object", + "properties": { + "f": { + "type": "number", + "format": "float" + } + }, + "required": [ + "f" + ], + "unevaluatedProperties": false, + "allOf": [ + { + "oneOf": [ + { + "type": "object", + "properties": { + "B": { + "type": "boolean" + } + }, + "required": [ + "B" + ] + }, + { + "type": "object", + "properties": { + "S": { + "type": "string" + } + }, + "required": [ + "S" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U" + ] + }, + { + "type": "object", + "properties": { + "F": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "B2": { + "type": "boolean" + } + }, + "required": [ + "B2" + ] + }, + { + "type": "object", + "properties": { + "S2": { + "type": "string" + } + }, + "required": [ + "S2" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U2": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U2" + ] + }, + { + "type": "object", + "properties": { + "F2": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F2" + ] + } + ] + } + ], + "oneOf": [ + { + "type": "object", + "properties": { + "B3": { + "type": "boolean" + } + }, + "required": [ + "B3" + ] + }, + { + "type": "object", + "properties": { + "S3": { + "type": "string" + } + }, + "required": [ + "S3" + ] + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum_flatten_duf_draft07.json b/schemars/tests/expected/enum_flatten_duf_draft07.json new file mode 100644 index 00000000..62323ddf --- /dev/null +++ b/schemars/tests/expected/enum_flatten_duf_draft07.json @@ -0,0 +1,161 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FlatDenyUnknownFields", + "type": "object", + "properties": { + "f": { + "type": "number", + "format": "float" + }, + "B": true, + "B2": true, + "B3": true, + "F": true, + "F2": true, + "S": true, + "S2": true, + "S3": true, + "U": true, + "U2": true + }, + "required": [ + "f" + ], + "allOf": [ + { + "oneOf": [ + { + "type": "object", + "properties": { + "B": { + "type": "boolean" + } + }, + "required": [ + "B" + ] + }, + { + "type": "object", + "properties": { + "S": { + "type": "string" + } + }, + "required": [ + "S" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U" + ] + }, + { + "type": "object", + "properties": { + "F": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "B2": { + "type": "boolean" + } + }, + "required": [ + "B2" + ] + }, + { + "type": "object", + "properties": { + "S2": { + "type": "string" + } + }, + "required": [ + "S2" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "U2": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "U2" + ] + }, + { + "type": "object", + "properties": { + "F2": { + "type": "number", + "format": "double" + } + }, + "required": [ + "F2" + ] + } + ] + } + ], + "oneOf": [ + { + "type": "object", + "properties": { + "B3": { + "type": "boolean" + } + }, + "required": [ + "B3" + ] + }, + { + "type": "object", + "properties": { + "S3": { + "type": "string" + } + }, + "required": [ + "S3" + ] + } + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/schemars/tests/expected/enumset.json b/schemars/tests/expected/enumset.json index 4950c939..72a39a19 100644 --- a/schemars/tests/expected/enumset.json +++ b/schemars/tests/expected/enumset.json @@ -1,12 +1,12 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Set_of_Foo", "type": "array", + "uniqueItems": true, "items": { - "$ref": "#/definitions/Foo" + "$ref": "#/$defs/Foo" }, - "uniqueItems": true, - "definitions": { + "$defs": { "Foo": { "type": "string", "enum": [ @@ -15,4 +15,4 @@ ] } } -} +} \ No newline at end of file diff --git a/schemars/tests/expected/examples.json b/schemars/tests/expected/examples.json index c02a139a..b891aa0b 100644 --- a/schemars/tests/expected/examples.json +++ b/schemars/tests/expected/examples.json @@ -1,39 +1,39 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", - "examples": [ - { - "bar": false, - "baz": null, - "foo": 0 - }, - null - ], "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { + "type": "integer", + "format": "int32", "examples": [ 8, null - ], - "type": "integer", - "format": "int32" + ] }, "bar": { "type": "boolean" }, "baz": { - "examples": [ - null - ], "type": [ "string", "null" + ], + "examples": [ + null ] } - } + }, + "required": [ + "foo", + "bar" + ], + "examples": [ + { + "foo": 0, + "bar": false, + "baz": null + }, + null + ] } \ No newline at end of file diff --git a/schemars/tests/expected/extend_enum_adjacent.json b/schemars/tests/expected/extend_enum_adjacent.json new file mode 100644 index 00000000..6241e079 --- /dev/null +++ b/schemars/tests/expected/extend_enum_adjacent.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Adjacent", + "oneOf": [ + { + "type": "object", + "properties": { + "t": { + "type": "string", + "enum": [ + "Unit" + ] + } + }, + "required": [ + "t" + ], + "foo": "bar" + }, + { + "type": "object", + "properties": { + "t": { + "type": "string", + "enum": [ + "NewType" + ] + }, + "c": true + }, + "required": [ + "t", + "c" + ], + "foo": "bar" + }, + { + "type": "object", + "properties": { + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + }, + "c": { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "boolean" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "t", + "c" + ], + "foo": "bar" + }, + { + "type": "object", + "properties": { + "t": { + "type": "string", + "enum": [ + "Struct" + ] + }, + "c": { + "type": "object", + "properties": { + "i": { + "type": "integer", + "format": "int32" + }, + "b": { + "type": "boolean" + } + }, + "required": [ + "i", + "b" + ] + } + }, + "required": [ + "t", + "c" + ], + "foo": "bar" + } + ], + "foo": "bar" +} \ No newline at end of file diff --git a/schemars/tests/expected/extend_enum_external.json b/schemars/tests/expected/extend_enum_external.json new file mode 100644 index 00000000..c15d47f5 --- /dev/null +++ b/schemars/tests/expected/extend_enum_external.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "External", + "oneOf": [ + { + "type": "string", + "const": "Unit", + "foo": "bar" + }, + { + "type": "object", + "properties": { + "NewType": true + }, + "required": [ + "NewType" + ], + "additionalProperties": false, + "foo": "bar" + }, + { + "type": "object", + "properties": { + "Tuple": { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "boolean" + } + ], + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "Tuple" + ], + "additionalProperties": false, + "foo": "bar" + }, + { + "type": "object", + "properties": { + "Struct": { + "type": "object", + "properties": { + "i": { + "type": "integer", + "format": "int32" + }, + "b": { + "type": "boolean" + } + }, + "required": [ + "i", + "b" + ] + } + }, + "required": [ + "Struct" + ], + "additionalProperties": false, + "foo": "bar" + } + ], + "foo": "bar" +} \ No newline at end of file diff --git a/schemars/tests/expected/extend_enum_internal.json b/schemars/tests/expected/extend_enum_internal.json new file mode 100644 index 00000000..0dee8174 --- /dev/null +++ b/schemars/tests/expected/extend_enum_internal.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Internal", + "oneOf": [ + { + "type": "object", + "properties": { + "typeProperty": { + "type": "string", + "const": "Unit" + } + }, + "required": [ + "typeProperty" + ], + "foo": "bar" + }, + { + "type": "object", + "properties": { + "typeProperty": { + "type": "string", + "const": "NewType" + } + }, + "required": [ + "typeProperty" + ], + "foo": "bar" + }, + { + "type": "object", + "properties": { + "i": { + "type": "integer", + "format": "int32" + }, + "b": { + "type": "boolean" + }, + "typeProperty": { + "type": "string", + "const": "Struct" + } + }, + "required": [ + "typeProperty", + "i", + "b" + ], + "foo": "bar" + } + ], + "foo": "bar" +} \ No newline at end of file diff --git a/schemars/tests/expected/extend_enum_untagged.json b/schemars/tests/expected/extend_enum_untagged.json new file mode 100644 index 00000000..4f733fe9 --- /dev/null +++ b/schemars/tests/expected/extend_enum_untagged.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Untagged", + "anyOf": [ + { + "type": "null", + "foo": "bar" + }, + { + "foo": "bar" + }, + { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "boolean" + } + ], + "minItems": 2, + "maxItems": 2, + "foo": "bar" + }, + { + "type": "object", + "properties": { + "i": { + "type": "integer", + "format": "int32" + }, + "b": { + "type": "boolean" + } + }, + "required": [ + "i", + "b" + ], + "foo": "bar" + } + ], + "foo": "bar" +} \ No newline at end of file diff --git a/schemars/tests/expected/extend_struct.json b/schemars/tests/expected/extend_struct.json new file mode 100644 index 00000000..fc7dd50f --- /dev/null +++ b/schemars/tests/expected/extend_struct.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct", + "type": "object", + "properties": { + "value": { + "foo": "bar" + }, + "int": { + "type": "overridden", + "format": "int32" + } + }, + "required": [ + "value", + "int" + ], + "msg": "hello world", + "obj": { + "array": [ + null, + null + ] + }, + "3": 3.0, + "pi": 3.14 +} \ No newline at end of file diff --git a/schemars/tests/expected/flatten.json b/schemars/tests/expected/flatten.json index fedf571a..7dfd54e0 100644 --- a/schemars/tests/expected/flatten.json +++ b/schemars/tests/expected/flatten.json @@ -1,13 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Flat", "type": "object", - "required": [ - "b", - "f", - "s", - "v" - ], "properties": { "f": { "type": "number", @@ -20,8 +14,8 @@ "type": "string" }, "os": { - "default": "", - "type": "string" + "type": "string", + "default": "" }, "v": { "type": "array", @@ -30,5 +24,11 @@ "format": "int32" } } - } + }, + "required": [ + "f", + "b", + "s", + "v" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/flattened_value.json b/schemars/tests/expected/flattened_value.json new file mode 100644 index 00000000..fe79d426 --- /dev/null +++ b/schemars/tests/expected/flattened_value.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "FlattenValue", + "type": "object", + "properties": { + "flag": { + "type": "boolean" + } + }, + "required": [ + "flag" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemars/tests/expected/from_json_value.json b/schemars/tests/expected/from_json_value.json index 0d94b66a..b217fadf 100644 --- a/schemars/tests/expected/from_json_value.json +++ b/schemars/tests/expected/from_json_value.json @@ -1,29 +1,22 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "examples": [ - { - "bool": true, - "minusOne": -1, - "null": null, - "object": { - "array": [ - "foo", - "bar" - ] - }, - "one": 1, - "zero": 0, - "zeroPointZero": 0.0 - } - ], + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { - "bool": { - "type": "boolean" + "zero": { + "type": "integer" + }, + "one": { + "type": "integer" }, "minusOne": { "type": "integer" }, + "zeroPointZero": { + "type": "number" + }, + "bool": { + "type": "boolean" + }, "null": true, "object": { "type": "object", @@ -35,15 +28,22 @@ } } } - }, - "one": { - "type": "integer" - }, - "zero": { - "type": "integer" - }, - "zeroPointZero": { - "type": "number" } - } + }, + "examples": [ + { + "zero": 0, + "one": 1, + "minusOne": -1, + "zeroPointZero": 0.0, + "bool": true, + "null": null, + "object": { + "array": [ + "foo", + "bar" + ] + } + } + ] } \ No newline at end of file diff --git a/schemars/tests/expected/from_value_2019_09.json b/schemars/tests/expected/from_value_2019_09.json index 9cea709c..939410da 100644 --- a/schemars/tests/expected/from_value_2019_09.json +++ b/schemars/tests/expected/from_value_2019_09.json @@ -1,28 +1,6 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", "title": "MyStruct", - "examples": [ - { - "myBool": true, - "myInnerStruct": { - "my_empty_map": {}, - "my_empty_vec": [], - "my_map": { - "": 0.0 - }, - "my_tuple": [ - "💩", - 42 - ], - "my_vec": [ - "hello", - "world" - ] - }, - "myInt": 123, - "myNullableEnum": null - } - ], "type": "object", "properties": { "myInt": { @@ -57,20 +35,42 @@ }, "my_tuple": { "type": "array", + "minItems": 2, + "maxItems": 2, "items": [ { "type": "string", - "maxLength": 1, - "minLength": 1 + "minLength": 1, + "maxLength": 1 }, { "type": "integer" } - ], - "maxItems": 2, - "minItems": 2 + ] } } } - } + }, + "examples": [ + { + "myInt": 123, + "myBool": true, + "myNullableEnum": null, + "myInnerStruct": { + "my_map": { + "": 0.0 + }, + "my_vec": [ + "hello", + "world" + ], + "my_empty_map": {}, + "my_empty_vec": [], + "my_tuple": [ + "💩", + 42 + ] + } + } + ] } \ No newline at end of file diff --git a/schemars/tests/expected/from_value_draft07.json b/schemars/tests/expected/from_value_draft07.json index dec39962..721e8716 100644 --- a/schemars/tests/expected/from_value_draft07.json +++ b/schemars/tests/expected/from_value_draft07.json @@ -1,28 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MyStruct", - "examples": [ - { - "myBool": true, - "myInnerStruct": { - "my_empty_map": {}, - "my_empty_vec": [], - "my_map": { - "": 0.0 - }, - "my_tuple": [ - "💩", - 42 - ], - "my_vec": [ - "hello", - "world" - ] - }, - "myInt": 123, - "myNullableEnum": null - } - ], "type": "object", "properties": { "myInt": { @@ -57,20 +35,42 @@ }, "my_tuple": { "type": "array", + "minItems": 2, + "maxItems": 2, "items": [ { "type": "string", - "maxLength": 1, - "minLength": 1 + "minLength": 1, + "maxLength": 1 }, { "type": "integer" } - ], - "maxItems": 2, - "minItems": 2 + ] } } } - } + }, + "examples": [ + { + "myInt": 123, + "myBool": true, + "myNullableEnum": null, + "myInnerStruct": { + "my_map": { + "": 0.0 + }, + "my_vec": [ + "hello", + "world" + ], + "my_empty_map": {}, + "my_empty_vec": [], + "my_tuple": [ + "💩", + 42 + ] + } + } + ] } \ No newline at end of file diff --git a/schemars/tests/expected/from_value_openapi3.json b/schemars/tests/expected/from_value_openapi3.json index 132a59a2..88f08a79 100644 --- a/schemars/tests/expected/from_value_openapi3.json +++ b/schemars/tests/expected/from_value_openapi3.json @@ -1,5 +1,5 @@ { - "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", + "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", "title": "MyStruct", "type": "object", "properties": { @@ -37,40 +37,40 @@ }, "my_tuple": { "type": "array", + "minItems": 2, + "maxItems": 2, "items": [ { "type": "string", - "maxLength": 1, - "minLength": 1 + "minLength": 1, + "maxLength": 1 }, { "type": "integer" } - ], - "maxItems": 2, - "minItems": 2 + ] } } } }, "example": { + "myInt": 123, "myBool": true, + "myNullableEnum": null, "myInnerStruct": { - "my_empty_map": {}, - "my_empty_vec": [], "my_map": { "": 0.0 }, - "my_tuple": [ - "💩", - 42 - ], "my_vec": [ "hello", "world" + ], + "my_empty_map": {}, + "my_empty_vec": [], + "my_tuple": [ + "💩", + 42 ] - }, - "myInt": 123, - "myNullableEnum": null + } } } \ No newline at end of file diff --git a/schemars/tests/expected/garde.json b/schemars/tests/expected/garde.json new file mode 100644 index 00000000..1f5d8a64 --- /dev/null +++ b/schemars/tests/expected/garde.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct", + "type": "object", + "properties": { + "min_max": { + "type": "number", + "format": "float", + "minimum": 0.01, + "maximum": 100 + }, + "min_max2": { + "type": "number", + "format": "float", + "minimum": 1, + "maximum": 1000 + }, + "regex_str1": { + "type": "string", + "pattern": "^[Hh]ello\\b" + }, + "contains_str1": { + "type": "string", + "pattern": "substring\\.\\.\\." + }, + "email_address": { + "type": "string", + "format": "email" + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "non_empty_str": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "non_empty_str2": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "pair": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "minItems": 2, + "maxItems": 2 + }, + "required_option": { + "type": "boolean" + }, + "x": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "min_max", + "min_max2", + "regex_str1", + "contains_str1", + "email_address", + "homepage", + "non_empty_str", + "non_empty_str2", + "pair", + "required_option", + "x" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_newtype.json b/schemars/tests/expected/garde_newtype.json new file mode 100644 index 00000000..cd835f53 --- /dev/null +++ b/schemars/tests/expected/garde_newtype.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "NewType", + "type": "integer", + "format": "uint8", + "minimum": 0, + "maximum": 10 +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_schemars_attrs.json b/schemars/tests/expected/garde_schemars_attrs.json new file mode 100644 index 00000000..f5487060 --- /dev/null +++ b/schemars/tests/expected/garde_schemars_attrs.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct2", + "type": "object", + "properties": { + "min_max": { + "type": "number", + "format": "float", + "minimum": 0.01, + "maximum": 100 + }, + "min_max2": { + "type": "number", + "format": "float", + "minimum": 1, + "maximum": 1000 + }, + "regex_str1": { + "type": "string", + "pattern": "^[Hh]ello\\b" + }, + "contains_str1": { + "type": "string", + "pattern": "substring\\.\\.\\." + }, + "email_address": { + "type": "string", + "format": "email" + }, + "homepage": { + "type": "string", + "format": "uri" + }, + "non_empty_str": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "non_empty_str2": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "pair": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "minItems": 2, + "maxItems": 2 + }, + "required_option": { + "type": "boolean" + }, + "x": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "min_max", + "min_max2", + "regex_str1", + "contains_str1", + "email_address", + "homepage", + "non_empty_str", + "non_empty_str2", + "pair", + "required_option", + "x" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_tuple.json b/schemars/tests/expected/garde_tuple.json new file mode 100644 index 00000000..fa812242 --- /dev/null +++ b/schemars/tests/expected/garde_tuple.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Tuple", + "type": "array", + "prefixItems": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0, + "maximum": 10 + }, + { + "type": "boolean" + } + ], + "minItems": 2, + "maxItems": 2 +} \ No newline at end of file diff --git a/schemars/tests/expected/indexmap.json b/schemars/tests/expected/indexmap.json index 98065d9d..8ba90a85 100644 --- a/schemars/tests/expected/indexmap.json +++ b/schemars/tests/expected/indexmap.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "IndexMapTypes", "type": "object", - "required": [ - "map", - "set" - ], "properties": { "map": { "type": "object", @@ -15,11 +11,15 @@ }, "set": { "type": "array", + "uniqueItems": true, "items": { "type": "integer", "format": "int" - }, - "uniqueItems": true + } } - } + }, + "required": [ + "map", + "set" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/inline-subschemas-recursive.json b/schemars/tests/expected/inline-subschemas-recursive.json index e051e5df..21f7f315 100644 --- a/schemars/tests/expected/inline-subschemas-recursive.json +++ b/schemars/tests/expected/inline-subschemas-recursive.json @@ -1,12 +1,12 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "RecursiveOuter", "type": "object", "properties": { "direct": { "anyOf": [ { - "$ref": "#/definitions/RecursiveOuter" + "$ref": "#/$defs/RecursiveOuter" }, { "type": "null" @@ -18,24 +18,24 @@ "object", "null" ], - "required": [ - "recursive" - ], "properties": { "recursive": { - "$ref": "#/definitions/RecursiveOuter" + "$ref": "#/$defs/RecursiveOuter" } - } + }, + "required": [ + "recursive" + ] } }, - "definitions": { + "$defs": { "RecursiveOuter": { "type": "object", "properties": { "direct": { "anyOf": [ { - "$ref": "#/definitions/RecursiveOuter" + "$ref": "#/$defs/RecursiveOuter" }, { "type": "null" @@ -47,14 +47,14 @@ "object", "null" ], - "required": [ - "recursive" - ], "properties": { "recursive": { - "$ref": "#/definitions/RecursiveOuter" + "$ref": "#/$defs/RecursiveOuter" } - } + }, + "required": [ + "recursive" + ] } } } diff --git a/schemars/tests/expected/inline-subschemas.json b/schemars/tests/expected/inline-subschemas.json index b7315534..cbee457a 100644 --- a/schemars/tests/expected/inline-subschemas.json +++ b/schemars/tests/expected/inline-subschemas.json @@ -1,23 +1,23 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyJob", "type": "object", - "required": [ - "spec" - ], "properties": { "spec": { "type": "object", - "required": [ - "replicas" - ], "properties": { "replicas": { "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0 } - } + }, + "required": [ + "replicas" + ] } - } + }, + "required": [ + "spec" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/macro_built_enum.json b/schemars/tests/expected/macro_built_enum.json index d030787d..b27df44a 100644 --- a/schemars/tests/expected/macro_built_enum.json +++ b/schemars/tests/expected/macro_built_enum.json @@ -1,32 +1,32 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "OuterEnum", "oneOf": [ { "type": "object", - "required": [ - "InnerStruct" - ], "properties": { "InnerStruct": { - "$ref": "#/definitions/InnerStruct" + "$ref": "#/$defs/InnerStruct" } }, + "required": [ + "InnerStruct" + ], "additionalProperties": false } ], - "definitions": { + "$defs": { "InnerStruct": { "type": "object", - "required": [ - "x" - ], "properties": { "x": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "x" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/macro_built_struct.json b/schemars/tests/expected/macro_built_struct.json index 811eae31..1d0fd083 100644 --- a/schemars/tests/expected/macro_built_struct.json +++ b/schemars/tests/expected/macro_built_struct.json @@ -1,20 +1,20 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", - "required": [ - "v", - "x" - ], "properties": { "x": { "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0 }, "v": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "x", + "v" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/no-variants.json b/schemars/tests/expected/no-variants.json new file mode 100644 index 00000000..14de7f4e --- /dev/null +++ b/schemars/tests/expected/no-variants.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "NoVariants", + "type": "string", + "enum": [] +} \ No newline at end of file diff --git a/schemars/tests/expected/no_std.json b/schemars/tests/expected/no_std.json new file mode 100644 index 00000000..68da8900 --- /dev/null +++ b/schemars/tests/expected/no_std.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "MyStruct", + "type": "object", + "properties": { + "my_int": { + "type": "integer", + "format": "int32" + }, + "my_bool": { + "type": "boolean" + }, + "my_nullable_enum": { + "anyOf": [ + { + "$ref": "#/$defs/MyEnum" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "my_int", + "my_bool" + ], + "$defs": { + "MyEnum": { + "oneOf": [ + { + "type": "object", + "properties": { + "StringNewType": { + "type": "string" + } + }, + "required": [ + "StringNewType" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "StructVariant": { + "type": "object", + "properties": { + "floats": { + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + }, + "required": [ + "floats" + ] + } + }, + "required": [ + "StructVariant" + ], + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/nonzero_ints.json b/schemars/tests/expected/nonzero_ints.json index cbfcfd97..432e3b04 100644 --- a/schemars/tests/expected/nonzero_ints.json +++ b/schemars/tests/expected/nonzero_ints.json @@ -1,23 +1,17 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "nonzero_signed", - "nonzero_unsigned", - "signed", - "unsigned" - ], "properties": { "unsigned": { "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0 }, "nonzero_unsigned": { "type": "integer", "format": "uint32", - "minimum": 1.0 + "minimum": 1 }, "signed": { "type": "integer", @@ -30,5 +24,11 @@ "const": 0 } } - } + }, + "required": [ + "unsigned", + "nonzero_unsigned", + "signed", + "nonzero_signed" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/os_strings.json b/schemars/tests/expected/os_strings.json index d3336757..72e0be10 100644 --- a/schemars/tests/expected/os_strings.json +++ b/schemars/tests/expected/os_strings.json @@ -1,53 +1,53 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "OsStrings", "type": "object", - "required": [ - "borrowed", - "owned" - ], "properties": { "owned": { - "$ref": "#/definitions/OsString" + "$ref": "#/$defs/OsString" }, "borrowed": { - "$ref": "#/definitions/OsString" + "$ref": "#/$defs/OsString" } }, - "definitions": { + "required": [ + "owned", + "borrowed" + ], + "$defs": { "OsString": { "oneOf": [ { "type": "object", - "required": [ - "Unix" - ], "properties": { "Unix": { "type": "array", "items": { "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0 } } - } + }, + "required": [ + "Unix" + ] }, { "type": "object", - "required": [ - "Windows" - ], "properties": { "Windows": { "type": "array", "items": { "type": "integer", "format": "uint16", - "minimum": 0.0 + "minimum": 0 } } - } + }, + "required": [ + "Windows" + ] } ] } diff --git a/schemars/tests/expected/property-name-struct.json b/schemars/tests/expected/property-name-struct.json index aa708fde..82ea9ed8 100644 --- a/schemars/tests/expected/property-name-struct.json +++ b/schemars/tests/expected/property-name-struct.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "camelCase", - "new_name_1", - "new_name_2" - ], "properties": { "camelCase": { "type": "integer", @@ -20,5 +15,10 @@ "type": "integer", "format": "int32" } - } + }, + "required": [ + "camelCase", + "new_name_1", + "new_name_2" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/range.json b/schemars/tests/expected/range.json index a3b14afa..64db4bdc 100644 --- a/schemars/tests/expected/range.json +++ b/schemars/tests/expected/range.json @@ -1,49 +1,45 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "bound", - "inclusive", - "range" - ], "properties": { "range": { - "$ref": "#/definitions/Range_of_uint" + "$ref": "#/$defs/Range_of_uint" }, "inclusive": { - "$ref": "#/definitions/Range_of_double" + "$ref": "#/$defs/Range_of_double" }, "bound": { - "$ref": "#/definitions/Bound_of_String" + "$ref": "#/$defs/Bound_of_string" } }, - "definitions": { + "required": [ + "range", + "inclusive", + "bound" + ], + "$defs": { "Range_of_uint": { "type": "object", - "required": [ - "end", - "start" - ], "properties": { "start": { "type": "integer", "format": "uint", - "minimum": 0.0 + "minimum": 0 }, "end": { "type": "integer", "format": "uint", - "minimum": 0.0 + "minimum": 0 } - } + }, + "required": [ + "start", + "end" + ] }, "Range_of_double": { "type": "object", - "required": [ - "end", - "start" - ], "properties": { "start": { "type": "number", @@ -53,31 +49,35 @@ "type": "number", "format": "double" } - } + }, + "required": [ + "start", + "end" + ] }, - "Bound_of_String": { + "Bound_of_string": { "oneOf": [ { "type": "object", - "required": [ - "Included" - ], "properties": { "Included": { "type": "string" } - } + }, + "required": [ + "Included" + ] }, { "type": "object", - "required": [ - "Excluded" - ], "properties": { "Excluded": { "type": "string" } - } + }, + "required": [ + "Excluded" + ] }, { "type": "string", diff --git a/schemars/tests/expected/remote_derive.json b/schemars/tests/expected/remote_derive.json index 6f09eb22..760630d1 100644 --- a/schemars/tests/expected/remote_derive.json +++ b/schemars/tests/expected/remote_derive.json @@ -1,45 +1,33 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Process", "type": "object", - "required": [ - "command_line", - "wall_time" - ], "properties": { "command_line": { "type": "string" }, "wall_time": { - "$ref": "#/definitions/Duration" + "$ref": "#/$defs/Duration" }, "user_cpu_time": { + "$ref": "#/$defs/Duration", "default": { - "nanos": 0, - "secs": 0 - }, - "allOf": [ - { - "$ref": "#/definitions/Duration" - } - ] + "secs": 0, + "nanos": 0 + } }, "system_cpu_time": { - "default": "0.000000000s", - "allOf": [ - { - "$ref": "#/definitions/Duration" - } - ] + "$ref": "#/$defs/Duration", + "default": "0.000000000s" } }, - "definitions": { + "required": [ + "command_line", + "wall_time" + ], + "$defs": { "Duration": { "type": "object", - "required": [ - "nanos", - "secs" - ], "properties": { "secs": { "type": "integer", @@ -49,7 +37,11 @@ "type": "integer", "format": "int32" } - } + }, + "required": [ + "secs", + "nanos" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/remote_derive_generic.json b/schemars/tests/expected/remote_derive_generic.json index 2fea80a3..bef4d6ac 100644 --- a/schemars/tests/expected/remote_derive_generic.json +++ b/schemars/tests/expected/remote_derive_generic.json @@ -1,48 +1,48 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct_for_int32", "type": "object", - "required": [ - "byte_or_bool2", - "fake_map", - "s", - "unit_or_t2" - ], "properties": { "byte_or_bool2": { - "$ref": "#/definitions/Or_for_uint8_and_Boolean" + "$ref": "#/$defs/Or_for_uint8_and_boolean" }, "unit_or_t2": { - "$ref": "#/definitions/Or_for_Null_and_int32" + "$ref": "#/$defs/Or_for_null_and_int32" }, "s": { - "$ref": "#/definitions/Str" + "$ref": "#/$defs/Str" }, "fake_map": { "type": "object", "additionalProperties": { "type": "array", + "uniqueItems": true, "items": { "type": "string" - }, - "uniqueItems": true + } } } }, - "definitions": { - "Or_for_uint8_and_Boolean": { + "required": [ + "byte_or_bool2", + "unit_or_t2", + "s", + "fake_map" + ], + "$defs": { + "Or_for_uint8_and_boolean": { "anyOf": [ { "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0 }, { "type": "boolean" } ] }, - "Or_for_Null_and_int32": { + "Or_for_null_and_int32": { "anyOf": [ { "type": "null" diff --git a/schemars/tests/expected/result.json b/schemars/tests/expected/result.json index d8d6ec17..b468835c 100644 --- a/schemars/tests/expected/result.json +++ b/schemars/tests/expected/result.json @@ -1,38 +1,35 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Container", "type": "object", - "required": [ - "result1", - "result2" - ], "properties": { "result1": { - "$ref": "#/definitions/Result_of_MyStruct_or_Array_of_String" + "$ref": "#/$defs/Result_of_MyStruct_or_Array_of_string" }, "result2": { - "$ref": "#/definitions/Result_of_Boolean_or_Null" + "$ref": "#/$defs/Result_of_boolean_or_null" } }, - "definitions": { - "Result_of_MyStruct_or_Array_of_String": { + "required": [ + "result1", + "result2" + ], + "$defs": { + "Result_of_MyStruct_or_Array_of_string": { "oneOf": [ { "type": "object", - "required": [ - "Ok" - ], "properties": { "Ok": { - "$ref": "#/definitions/MyStruct" + "$ref": "#/$defs/MyStruct" } - } + }, + "required": [ + "Ok" + ] }, { "type": "object", - "required": [ - "Err" - ], "properties": { "Err": { "type": "array", @@ -40,45 +37,48 @@ "type": "string" } } - } + }, + "required": [ + "Err" + ] } ] }, "MyStruct": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "foo" + ] }, - "Result_of_Boolean_or_Null": { + "Result_of_boolean_or_null": { "oneOf": [ { "type": "object", - "required": [ - "Ok" - ], "properties": { "Ok": { "type": "boolean" } - } + }, + "required": [ + "Ok" + ] }, { "type": "object", - "required": [ - "Err" - ], "properties": { "Err": { "type": "null" } - } + }, + "required": [ + "Err" + ] } ] } diff --git a/schemars/tests/expected/rust_decimal.json b/schemars/tests/expected/rust_decimal.json new file mode 100644 index 00000000..e94ca271 --- /dev/null +++ b/schemars/tests/expected/rust_decimal.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/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..fb65854d --- /dev/null +++ b/schemars/tests/expected/same_name.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Config2", + "type": "object", + "properties": { + "a_cfg": { + "$ref": "#/$defs/Config" + }, + "b_cfg": { + "$ref": "#/$defs/Config2" + } + }, + "required": [ + "a_cfg", + "b_cfg" + ], + "$defs": { + "Config": { + "type": "object", + "properties": { + "test": { + "type": "string" + } + }, + "required": [ + "test" + ] + }, + "Config2": { + "type": "object", + "properties": { + "test2": { + "type": "string" + } + }, + "required": [ + "test2" + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-2019_09.json b/schemars/tests/expected/schema-2019_09.json deleted file mode 100644 index cf11d352..00000000 --- a/schemars/tests/expected/schema-2019_09.json +++ /dev/null @@ -1,758 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "RootSchema", - "description": "The root object of a JSON Schema document.", - "type": "object", - "properties": { - "$schema": { - "description": "The `$schema` keyword.\n\nSee [JSON Schema 8.1.1. The \"$schema\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1).", - "type": [ - "string", - "null" - ] - }, - "definitions": { - "description": "The `definitions` keyword.\n\nIn JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still serialized as `definitions` for backward-compatibility.\n\nSee [JSON Schema 8.2.5. Schema Re-Use With \"$defs\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5), and [JSON Schema (draft 07) 9. Schema Re-Use With \"definitions\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_InstanceType" - }, - { - "type": "null" - } - ] - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": [ - "array", - "null" - ], - "items": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)" - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": [ - "string", - "null" - ] - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2)." - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": true - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": [ - "string", - "null" - ] - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_Schema" - }, - { - "type": "null" - } - ] - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": [ - "boolean", - "null" - ] - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true, - "definitions": { - "Schema": { - "description": "A JSON Schema.", - "anyOf": [ - { - "description": "A trivial boolean JSON Schema.\n\nThe schema `true` matches everything (always passes validation), whereas the schema `false` matches nothing (always fails validation).", - "type": "boolean" - }, - { - "description": "A JSON Schema object.", - "$ref": "#/definitions/SchemaObject" - } - ] - }, - "SchemaObject": { - "description": "A JSON Schema object.", - "type": "object", - "properties": { - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_InstanceType" - }, - { - "type": "null" - } - ] - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": [ - "array", - "null" - ], - "items": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)" - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": [ - "string", - "null" - ] - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2)." - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": true - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": [ - "string", - "null" - ] - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_Schema" - }, - { - "type": "null" - } - ] - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": [ - "boolean", - "null" - ] - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true - }, - "SingleOrVec_for_InstanceType": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/definitions/InstanceType" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/InstanceType" - } - } - ] - }, - "InstanceType": { - "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "type": "string", - "enum": [ - "null", - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "SingleOrVec_for_Schema": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/Schema" - } - } - ] - } - } -} \ 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..3c2edccb --- /dev/null +++ b/schemars/tests/expected/schema-name-const-generics.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "const-generics-z-42", + "type": "object", + "properties": { + "foo": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "foo" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-name-custom.json b/schemars/tests/expected/schema-name-custom.json index 866fb9d3..70b4d983 100644 --- a/schemars/tests/expected/schema-name-custom.json +++ b/schemars/tests/expected/schema-name-custom.json @@ -1,14 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "a-new-name-Array_of_String-int32-int32", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "a-new-name-Array_of_string-int32-int32", "type": "object", - "required": [ - "inner", - "t", - "u", - "v", - "w" - ], "properties": { "t": { "type": "integer", @@ -27,21 +20,28 @@ } }, "inner": { - "$ref": "#/definitions/another-new-name" + "$ref": "#/$defs/another-new-name" } }, - "definitions": { + "required": [ + "t", + "u", + "v", + "w", + "inner" + ], + "$defs": { "another-new-name": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "foo" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/schema-name-default.json b/schemars/tests/expected/schema-name-default.json index 31f7f267..2243ddd7 100644 --- a/schemars/tests/expected/schema-name-default.json +++ b/schemars/tests/expected/schema-name-default.json @@ -1,14 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "MyStruct_for_int32_and_null_and_boolean_and_Array_of_string", "type": "object", - "required": [ - "inner", - "t", - "u", - "v", - "w" - ], "properties": { "t": { "type": "integer", @@ -27,21 +20,28 @@ } }, "inner": { - "$ref": "#/definitions/MySimpleStruct" + "$ref": "#/$defs/MySimpleStruct" } }, - "definitions": { + "required": [ + "t", + "u", + "v", + "w", + "inner" + ], + "$defs": { "MySimpleStruct": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "foo" + ] } } } \ 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..85c6792e --- /dev/null +++ b/schemars/tests/expected/schema-name-mixed-generics.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "MixedGenericStruct_for_MyStruct_for_int32_and_null_and_boolean_and_Array_of_string_and_42_and_z", + "type": "object", + "properties": { + "generic": { + "$ref": "#/$defs/MyStruct_for_int32_and_null_and_boolean_and_Array_of_string" + }, + "foo": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "generic", + "foo" + ], + "$defs": { + "MyStruct_for_int32_and_null_and_boolean_and_Array_of_string": { + "type": "object", + "properties": { + "t": { + "type": "integer", + "format": "int32" + }, + "u": { + "type": "null" + }, + "v": { + "type": "boolean" + }, + "w": { + "type": "array", + "items": { + "type": "string" + } + }, + "inner": { + "$ref": "#/$defs/MySimpleStruct" + } + }, + "required": [ + "t", + "u", + "v", + "w", + "inner" + ] + }, + "MySimpleStruct": { + "type": "object", + "properties": { + "foo": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "foo" + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-openapi3.json b/schemars/tests/expected/schema-openapi3.json deleted file mode 100644 index 0548828a..00000000 --- a/schemars/tests/expected/schema-openapi3.json +++ /dev/null @@ -1,636 +0,0 @@ -{ - "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", - "title": "RootSchema", - "description": "The root object of a JSON Schema document.", - "type": "object", - "properties": { - "$schema": { - "description": "The `$schema` keyword.\n\nSee [JSON Schema 8.1.1. The \"$schema\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1).", - "type": "string", - "nullable": true - }, - "definitions": { - "description": "The `definitions` keyword.\n\nIn JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still serialized as `definitions` for backward-compatibility.\n\nSee [JSON Schema 8.2.5. Schema Re-Use With \"$defs\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5), and [JSON Schema (draft 07) 9. Schema Re-Use With \"definitions\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9).", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Schema" - } - }, - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "allOf": [ - { - "$ref": "#/components/schemas/SingleOrVec_for_InstanceType" - } - ], - "nullable": true - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": "string", - "nullable": true - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": "array", - "items": {}, - "nullable": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)", - "nullable": true - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": "string", - "nullable": true - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": "string", - "nullable": true - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": "string", - "nullable": true - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": "string", - "nullable": true - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2).", - "nullable": true - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": {} - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": "number", - "format": "double", - "nullable": true - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": "number", - "format": "double", - "nullable": true - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": "number", - "format": "double", - "nullable": true - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": "number", - "format": "double", - "nullable": true - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": "number", - "format": "double", - "nullable": true - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": "string", - "nullable": true - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "allOf": [ - { - "$ref": "#/components/schemas/SingleOrVec_for_Schema" - } - ], - "nullable": true - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": "boolean", - "nullable": true - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - } - }, - "additionalProperties": true, - "definitions": { - "Schema": { - "description": "A JSON Schema.", - "anyOf": [ - { - "description": "A trivial boolean JSON Schema.\n\nThe schema `true` matches everything (always passes validation), whereas the schema `false` matches nothing (always fails validation).", - "type": "boolean" - }, - { - "description": "A JSON Schema object.", - "allOf": [ - { - "$ref": "#/components/schemas/SchemaObject" - } - ] - } - ] - }, - "SchemaObject": { - "description": "A JSON Schema object.", - "type": "object", - "properties": { - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "allOf": [ - { - "$ref": "#/components/schemas/SingleOrVec_for_InstanceType" - } - ], - "nullable": true - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": "string", - "nullable": true - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": "array", - "items": {}, - "nullable": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)", - "nullable": true - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": "string", - "nullable": true - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": "string", - "nullable": true - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": "string", - "nullable": true - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": "string", - "nullable": true - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2).", - "nullable": true - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": {} - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - }, - "nullable": true - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": "number", - "format": "double", - "nullable": true - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": "number", - "format": "double", - "nullable": true - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": "number", - "format": "double", - "nullable": true - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": "number", - "format": "double", - "nullable": true - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": "number", - "format": "double", - "nullable": true - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": "string", - "nullable": true - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "allOf": [ - { - "$ref": "#/components/schemas/SingleOrVec_for_Schema" - } - ], - "nullable": true - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": "boolean", - "nullable": true - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": "integer", - "format": "uint32", - "minimum": 0.0, - "nullable": true - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "allOf": [ - { - "$ref": "#/components/schemas/Schema" - } - ], - "nullable": true - } - }, - "additionalProperties": true - }, - "SingleOrVec_for_InstanceType": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/components/schemas/InstanceType" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/InstanceType" - } - } - ] - }, - "InstanceType": { - "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "type": "string", - "enum": [ - "null", - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "SingleOrVec_for_Schema": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/components/schemas/Schema" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Schema" - } - } - ] - } - } -} \ No newline at end of file diff --git a/schemars/tests/expected/schema.json b/schemars/tests/expected/schema.json deleted file mode 100644 index 6cf840bd..00000000 --- a/schemars/tests/expected/schema.json +++ /dev/null @@ -1,762 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "RootSchema", - "description": "The root object of a JSON Schema document.", - "type": "object", - "properties": { - "$schema": { - "description": "The `$schema` keyword.\n\nSee [JSON Schema 8.1.1. The \"$schema\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1).", - "type": [ - "string", - "null" - ] - }, - "definitions": { - "description": "The `definitions` keyword.\n\nIn JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still serialized as `definitions` for backward-compatibility.\n\nSee [JSON Schema 8.2.5. Schema Re-Use With \"$defs\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5), and [JSON Schema (draft 07) 9. Schema Re-Use With \"definitions\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_InstanceType" - }, - { - "type": "null" - } - ] - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": [ - "array", - "null" - ], - "items": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)" - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": [ - "string", - "null" - ] - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2)." - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": true - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": [ - "string", - "null" - ] - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_Schema" - }, - { - "type": "null" - } - ] - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": [ - "boolean", - "null" - ] - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true, - "definitions": { - "Schema": { - "description": "A JSON Schema.", - "anyOf": [ - { - "description": "A trivial boolean JSON Schema.\n\nThe schema `true` matches everything (always passes validation), whereas the schema `false` matches nothing (always fails validation).", - "type": "boolean" - }, - { - "description": "A JSON Schema object.", - "allOf": [ - { - "$ref": "#/definitions/SchemaObject" - } - ] - } - ] - }, - "SchemaObject": { - "description": "A JSON Schema object.", - "type": "object", - "properties": { - "type": { - "description": "The `type` keyword.\n\nSee [JSON Schema Validation 6.1.1. \"type\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_InstanceType" - }, - { - "type": "null" - } - ] - }, - "format": { - "description": "The `format` keyword.\n\nSee [JSON Schema Validation 7. A Vocabulary for Semantic Content With \"format\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "The `enum` keyword.\n\nSee [JSON Schema Validation 6.1.2. \"enum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)", - "type": [ - "array", - "null" - ], - "items": true - }, - "const": { - "description": "The `const` keyword.\n\nSee [JSON Schema Validation 6.1.3. \"const\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)" - }, - "$ref": { - "description": "The `$ref` keyword.\n\nSee [JSON Schema 8.2.4.1. Direct References with \"$ref\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).", - "type": [ - "string", - "null" - ] - }, - "$id": { - "description": "The `$id` keyword.\n\nSee [JSON Schema 8.2.2. The \"$id\" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The `title` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "description": { - "description": "The `description` keyword.\n\nSee [JSON Schema Validation 9.1. \"title\" and \"description\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).", - "type": [ - "string", - "null" - ] - }, - "default": { - "description": "The `default` keyword.\n\nSee [JSON Schema Validation 9.2. \"default\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2)." - }, - "deprecated": { - "description": "The `deprecated` keyword.\n\nSee [JSON Schema Validation 9.3. \"deprecated\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).", - "type": "boolean" - }, - "readOnly": { - "description": "The `readOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "writeOnly": { - "description": "The `writeOnly` keyword.\n\nSee [JSON Schema Validation 9.4. \"readOnly\" and \"writeOnly\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).", - "type": "boolean" - }, - "examples": { - "description": "The `examples` keyword.\n\nSee [JSON Schema Validation 9.5. \"examples\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).", - "type": "array", - "items": true - }, - "allOf": { - "description": "The `allOf` keyword.\n\nSee [JSON Schema 9.2.1.1. \"allOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "anyOf": { - "description": "The `anyOf` keyword.\n\nSee [JSON Schema 9.2.1.2. \"anyOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "oneOf": { - "description": "The `oneOf` keyword.\n\nSee [JSON Schema 9.2.1.3. \"oneOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Schema" - } - }, - "not": { - "description": "The `not` keyword.\n\nSee [JSON Schema 9.2.1.4. \"not\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "if": { - "description": "The `if` keyword.\n\nSee [JSON Schema 9.2.2.1. \"if\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "then": { - "description": "The `then` keyword.\n\nSee [JSON Schema 9.2.2.2. \"then\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "else": { - "description": "The `else` keyword.\n\nSee [JSON Schema 9.2.2.3. \"else\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "multipleOf": { - "description": "The `multipleOf` keyword.\n\nSee [JSON Schema Validation 6.2.1. \"multipleOf\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maximum": { - "description": "The `maximum` keyword.\n\nSee [JSON Schema Validation 6.2.2. \"maximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMaximum": { - "description": "The `exclusiveMaximum` keyword.\n\nSee [JSON Schema Validation 6.2.3. \"exclusiveMaximum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "minimum": { - "description": "The `minimum` keyword.\n\nSee [JSON Schema Validation 6.2.4. \"minimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "exclusiveMinimum": { - "description": "The `exclusiveMinimum` keyword.\n\nSee [JSON Schema Validation 6.2.5. \"exclusiveMinimum\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).", - "type": [ - "number", - "null" - ], - "format": "double" - }, - "maxLength": { - "description": "The `maxLength` keyword.\n\nSee [JSON Schema Validation 6.3.1. \"maxLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minLength": { - "description": "The `minLength` keyword.\n\nSee [JSON Schema Validation 6.3.2. \"minLength\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "pattern": { - "description": "The `pattern` keyword.\n\nSee [JSON Schema Validation 6.3.3. \"pattern\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).", - "type": [ - "string", - "null" - ] - }, - "items": { - "description": "The `items` keyword.\n\nSee [JSON Schema 9.3.1.1. \"items\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).", - "anyOf": [ - { - "$ref": "#/definitions/SingleOrVec_for_Schema" - }, - { - "type": "null" - } - ] - }, - "additionalItems": { - "description": "The `additionalItems` keyword.\n\nSee [JSON Schema 9.3.1.2. \"additionalItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxItems": { - "description": "The `maxItems` keyword.\n\nSee [JSON Schema Validation 6.4.1. \"maxItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minItems": { - "description": "The `minItems` keyword.\n\nSee [JSON Schema Validation 6.4.2. \"minItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "uniqueItems": { - "description": "The `uniqueItems` keyword.\n\nSee [JSON Schema Validation 6.4.3. \"uniqueItems\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).", - "type": [ - "boolean", - "null" - ] - }, - "contains": { - "description": "The `contains` keyword.\n\nSee [JSON Schema 9.3.1.4. \"contains\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "maxProperties": { - "description": "The `maxProperties` keyword.\n\nSee [JSON Schema Validation 6.5.1. \"maxProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "minProperties": { - "description": "The `minProperties` keyword.\n\nSee [JSON Schema Validation 6.5.2. \"minProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "required": { - "description": "The `required` keyword.\n\nSee [JSON Schema Validation 6.5.3. \"required\"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "properties": { - "description": "The `properties` keyword.\n\nSee [JSON Schema 9.3.2.1. \"properties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "patternProperties": { - "description": "The `patternProperties` keyword.\n\nSee [JSON Schema 9.3.2.2. \"patternProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Schema" - } - }, - "additionalProperties": { - "description": "The `additionalProperties` keyword.\n\nSee [JSON Schema 9.3.2.3. \"additionalProperties\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - }, - "propertyNames": { - "description": "The `propertyNames` keyword.\n\nSee [JSON Schema 9.3.2.5. \"propertyNames\"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": true - }, - "SingleOrVec_for_InstanceType": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/definitions/InstanceType" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/InstanceType" - } - } - ] - }, - "InstanceType": { - "description": "The possible types of values in JSON Schema documents.\n\nSee [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).", - "type": "string", - "enum": [ - "null", - "boolean", - "object", - "array", - "number", - "string", - "integer" - ] - }, - "SingleOrVec_for_Schema": { - "description": "A type which can be serialized as a single item, or multiple items.\n\nIn some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.", - "anyOf": [ - { - "$ref": "#/definitions/Schema" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/Schema" - } - } - ] - } - } -} \ No newline at end of file diff --git a/schemars/tests/expected/schema_settings-2019_09.json b/schemars/tests/expected/schema_settings-2019_09.json new file mode 100644 index 00000000..bc99f155 --- /dev/null +++ b/schemars/tests/expected/schema_settings-2019_09.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "Outer", + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32", + "examples": [ + 8, + null + ] + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "value": true, + "inner": { + "anyOf": [ + { + "$ref": "#/$defs/Inner" + }, + { + "type": "null" + } + ] + }, + "tuples": { + "type": "array", + "items": { + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + { + "type": "integer", + "format": "int64" + } + ] + } + } + }, + "required": [ + "int", + "values", + "value", + "tuples" + ], + "$defs": { + "Inner": { + "oneOf": [ + { + "type": "string", + "enum": [ + "UndocumentedUnit1", + "UndocumentedUnit2" + ] + }, + { + "description": "This is a documented unit variant", + "type": "string", + "const": "DocumentedUnit" + }, + { + "type": "object", + "properties": { + "ValueNewType": true + }, + "required": [ + "ValueNewType" + ], + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_settings-2020_12.json b/schemars/tests/expected/schema_settings-2020_12.json new file mode 100644 index 00000000..79640586 --- /dev/null +++ b/schemars/tests/expected/schema_settings-2020_12.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Outer", + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32", + "examples": [ + 8, + null + ] + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "value": true, + "inner": { + "anyOf": [ + { + "$ref": "#/$defs/Inner" + }, + { + "type": "null" + } + ] + }, + "tuples": { + "type": "array", + "items": { + "type": "array", + "prefixItems": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + { + "type": "integer", + "format": "int64" + } + ], + "minItems": 2, + "maxItems": 2 + } + } + }, + "required": [ + "int", + "values", + "value", + "tuples" + ], + "$defs": { + "Inner": { + "oneOf": [ + { + "type": "string", + "enum": [ + "UndocumentedUnit1", + "UndocumentedUnit2" + ] + }, + { + "description": "This is a documented unit variant", + "type": "string", + "const": "DocumentedUnit" + }, + { + "type": "object", + "properties": { + "ValueNewType": true + }, + "required": [ + "ValueNewType" + ], + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_settings-openapi3.json b/schemars/tests/expected/schema_settings-openapi3.json new file mode 100644 index 00000000..8365822f --- /dev/null +++ b/schemars/tests/expected/schema_settings-openapi3.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema", + "title": "Outer", + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32", + "example": 8 + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "value": {}, + "inner": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Inner" + } + ] + }, + "tuples": { + "type": "array", + "items": { + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + { + "type": "integer", + "format": "int64" + } + ] + } + } + }, + "required": [ + "int", + "values", + "value", + "tuples" + ], + "components": { + "schemas": { + "Inner": { + "oneOf": [ + { + "type": "string", + "enum": [ + "UndocumentedUnit1", + "UndocumentedUnit2" + ] + }, + { + "description": "This is a documented unit variant", + "type": "string", + "enum": [ + "DocumentedUnit" + ] + }, + { + "type": "object", + "properties": { + "ValueNewType": {} + }, + "required": [ + "ValueNewType" + ], + "additionalProperties": false + } + ] + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_settings.json b/schemars/tests/expected/schema_settings.json new file mode 100644 index 00000000..4c435ad7 --- /dev/null +++ b/schemars/tests/expected/schema_settings.json @@ -0,0 +1,83 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Outer", + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32", + "examples": [ + 8, + null + ] + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "value": true, + "inner": { + "anyOf": [ + { + "$ref": "#/definitions/Inner" + }, + { + "type": "null" + } + ] + }, + "tuples": { + "type": "array", + "items": { + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + { + "type": "integer", + "format": "int64" + } + ] + } + } + }, + "required": [ + "int", + "values", + "value", + "tuples" + ], + "definitions": { + "Inner": { + "oneOf": [ + { + "type": "string", + "enum": [ + "UndocumentedUnit1", + "UndocumentedUnit2" + ] + }, + { + "description": "This is a documented unit variant", + "type": "string", + "const": "DocumentedUnit" + }, + { + "type": "object", + "properties": { + "ValueNewType": true + }, + "required": [ + "ValueNewType" + ], + "additionalProperties": false + } + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-adjacent-tagged.json b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json index 3ecba4c8..3e7173d5 100644 --- a/schemars/tests/expected/schema_with-enum-adjacent-tagged.json +++ b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json @@ -1,61 +1,63 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Adjacent", "oneOf": [ { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Struct" + ] + }, "c": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "boolean" } - } - }, - "t": { - "type": "string", - "enum": [ - "Struct" + }, + "required": [ + "foo" ] } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "boolean" - }, "t": { "type": "string", "enum": [ "NewType" ] + }, + "c": { + "type": "boolean" } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + }, "c": { "type": "array", - "items": [ + "prefixItems": [ { "type": "boolean" }, @@ -64,34 +66,32 @@ "format": "int32" } ], - "maxItems": 2, - "minItems": 2 - }, - "t": { - "type": "string", - "enum": [ - "Tuple" - ] + "minItems": 2, + "maxItems": 2 } - } + }, + "required": [ + "t", + "c" + ] }, { "type": "object", - "required": [ - "c", - "t" - ], "properties": { - "c": { - "type": "boolean" - }, "t": { "type": "string", "enum": [ "Unit" ] + }, + "c": { + "type": "boolean" } - } + }, + "required": [ + "t", + "c" + ] } ] } \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-external.json b/schemars/tests/expected/schema_with-enum-external.json index dea02199..5f5fe83e 100644 --- a/schemars/tests/expected/schema_with-enum-external.json +++ b/schemars/tests/expected/schema_with-enum-external.json @@ -1,48 +1,45 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "External", "oneOf": [ { "type": "object", - "required": [ - "struct" - ], "properties": { "struct": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "boolean" } - } + }, + "required": [ + "foo" + ] } }, + "required": [ + "struct" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "newType" - ], "properties": { "newType": { "type": "boolean" } }, + "required": [ + "newType" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "tuple" - ], "properties": { "tuple": { "type": "array", - "items": [ + "prefixItems": [ { "type": "boolean" }, @@ -51,22 +48,25 @@ "format": "int32" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } }, + "required": [ + "tuple" + ], "additionalProperties": false }, { "type": "object", - "required": [ - "unit" - ], "properties": { "unit": { "type": "boolean" } }, + "required": [ + "unit" + ], "additionalProperties": false } ] diff --git a/schemars/tests/expected/schema_with-enum-internal.json b/schemars/tests/expected/schema_with-enum-internal.json index 7ede7e6e..e6d707b9 100644 --- a/schemars/tests/expected/schema_with-enum-internal.json +++ b/schemars/tests/expected/schema_with-enum-internal.json @@ -1,58 +1,46 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Internal", "oneOf": [ { "type": "object", - "required": [ - "foo", - "typeProperty" - ], "properties": { "foo": { "type": "boolean" }, "typeProperty": { "type": "string", - "enum": [ - "Struct" - ] + "const": "Struct" } - } + }, + "required": [ + "typeProperty", + "foo" + ] }, { - "type": [ - "boolean", - "object" - ], - "required": [ - "typeProperty" - ], + "type": "object", "properties": { "typeProperty": { "type": "string", - "enum": [ - "NewType" - ] + "const": "NewType" } - } - }, - { - "type": [ - "boolean", - "object" - ], + }, "required": [ "typeProperty" - ], + ] + }, + { + "type": "object", "properties": { "typeProperty": { "type": "string", - "enum": [ - "Unit" - ] + "const": "Unit" } - } + }, + "required": [ + "typeProperty" + ] } ] } \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-untagged.json b/schemars/tests/expected/schema_with-enum-untagged.json index 55147ded..9057c87a 100644 --- a/schemars/tests/expected/schema_with-enum-untagged.json +++ b/schemars/tests/expected/schema_with-enum-untagged.json @@ -1,24 +1,24 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Untagged", "anyOf": [ { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "boolean" } - } + }, + "required": [ + "foo" + ] }, { "type": "boolean" }, { "type": "array", - "items": [ + "prefixItems": [ { "type": "boolean" }, @@ -27,8 +27,8 @@ "format": "int32" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 }, { "type": "boolean" diff --git a/schemars/tests/expected/schema_with-newtype.json b/schemars/tests/expected/schema_with-newtype.json index 8b709fb1..eb4a2f61 100644 --- a/schemars/tests/expected/schema_with-newtype.json +++ b/schemars/tests/expected/schema_with-newtype.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Newtype", "type": "boolean" } \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-struct.json b/schemars/tests/expected/schema_with-struct.json index feb5b511..0674f1e7 100644 --- a/schemars/tests/expected/schema_with-struct.json +++ b/schemars/tests/expected/schema_with-struct.json @@ -1,12 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", "type": "object", - "required": [ - "bar", - "baz", - "foo" - ], "properties": { "foo": { "type": "boolean" @@ -18,5 +13,10 @@ "baz": { "type": "boolean" } - } + }, + "required": [ + "foo", + "bar", + "baz" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-transparent-newtype.json b/schemars/tests/expected/schema_with-transparent-newtype.json index 9f6afb36..2aeff686 100644 --- a/schemars/tests/expected/schema_with-transparent-newtype.json +++ b/schemars/tests/expected/schema_with-transparent-newtype.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "schema_fn", "type": "boolean" } \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-tuple.json b/schemars/tests/expected/schema_with-tuple.json index 1a8f8ca0..1296e33f 100644 --- a/schemars/tests/expected/schema_with-tuple.json +++ b/schemars/tests/expected/schema_with-tuple.json @@ -1,8 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Tuple", "type": "array", - "items": [ + "prefixItems": [ { "type": "boolean" }, @@ -14,6 +14,6 @@ "type": "boolean" } ], - "maxItems": 3, - "minItems": 3 + "minItems": 3, + "maxItems": 3 } \ 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..9f5b1556 --- /dev/null +++ b/schemars/tests/expected/semver.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SemverTypes", + "type": "object", + "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-]+)*))?$" + } + }, + "required": [ + "version" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/skip_enum_variants.json b/schemars/tests/expected/skip_enum_variants.json index ba1bf23c..bd3a893b 100644 --- a/schemars/tests/expected/skip_enum_variants.json +++ b/schemars/tests/expected/skip_enum_variants.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyEnum", "oneOf": [ { @@ -10,15 +10,15 @@ }, { "type": "object", - "required": [ - "Included1" - ], "properties": { "Included1": { "type": "number", "format": "float" } }, + "required": [ + "Included1" + ], "additionalProperties": false } ] diff --git a/schemars/tests/expected/skip_struct_fields.json b/schemars/tests/expected/skip_struct_fields.json index d4885618..2a74c32d 100644 --- a/schemars/tests/expected/skip_struct_fields.json +++ b/schemars/tests/expected/skip_struct_fields.json @@ -1,24 +1,24 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", - "required": [ - "included", - "writable" - ], "properties": { "readable": { - "default": "", + "type": "string", "readOnly": true, - "type": "string" + "default": "" }, "writable": { - "writeOnly": true, "type": "number", - "format": "float" + "format": "float", + "writeOnly": true }, "included": { "type": "null" } - } + }, + "required": [ + "writable", + "included" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/skip_tuple_fields.json b/schemars/tests/expected/skip_tuple_fields.json index 9e5745b4..e3038bcc 100644 --- a/schemars/tests/expected/skip_tuple_fields.json +++ b/schemars/tests/expected/skip_tuple_fields.json @@ -1,16 +1,17 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "TupleStruct", "type": "array", - "items": [ + "prefixItems": [ { "type": "number", - "format": "float" + "format": "float", + "writeOnly": true }, { "type": "null" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } \ No newline at end of file diff --git a/schemars/tests/expected/smallvec.json b/schemars/tests/expected/smallvec.json index eab45c0a..be50ed71 100644 --- a/schemars/tests/expected/smallvec.json +++ b/schemars/tests/expected/smallvec.json @@ -1,6 +1,6 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_String", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Array_of_string", "type": "array", "items": { "type": "string" diff --git a/schemars/tests/expected/smol_str.json b/schemars/tests/expected/smol_str.json index 42f099a3..0604332c 100644 --- a/schemars/tests/expected/smol_str.json +++ b/schemars/tests/expected/smol_str.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "String", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "string", "type": "string" } \ No newline at end of file diff --git a/schemars/tests/expected/struct-newtype.json b/schemars/tests/expected/struct-newtype.json index 284e47db..e7b1cba9 100644 --- a/schemars/tests/expected/struct-newtype.json +++ b/schemars/tests/expected/struct-newtype.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Newtype", "type": "integer", "format": "int32" diff --git a/schemars/tests/expected/struct-normal-additional-properties.json b/schemars/tests/expected/struct-normal-additional-properties.json index 1a8d7494..2c55fb7d 100644 --- a/schemars/tests/expected/struct-normal-additional-properties.json +++ b/schemars/tests/expected/struct-normal-additional-properties.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -21,5 +17,9 @@ ] } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "foo", + "bar" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/struct-normal.json b/schemars/tests/expected/struct-normal.json index cee25b17..c49757c5 100644 --- a/schemars/tests/expected/struct-normal.json +++ b/schemars/tests/expected/struct-normal.json @@ -1,11 +1,7 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", "type": "object", - "required": [ - "bar", - "foo" - ], "properties": { "foo": { "type": "integer", @@ -20,5 +16,9 @@ "null" ] } - } + }, + "required": [ + "foo", + "bar" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/struct-tuple.json b/schemars/tests/expected/struct-tuple.json index ced169ea..89600688 100644 --- a/schemars/tests/expected/struct-tuple.json +++ b/schemars/tests/expected/struct-tuple.json @@ -1,8 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Tuple", "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "int32" @@ -17,6 +17,6 @@ ] } ], - "maxItems": 3, - "minItems": 3 + "minItems": 3, + "maxItems": 3 } \ No newline at end of file diff --git a/schemars/tests/expected/struct-unit.json b/schemars/tests/expected/struct-unit.json index 3252d20d..46f6fcf1 100644 --- a/schemars/tests/expected/struct-unit.json +++ b/schemars/tests/expected/struct-unit.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Unit", "type": "null" } \ No newline at end of file diff --git a/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json b/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json new file mode 100644 index 00000000..1ac94b54 --- /dev/null +++ b/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Tuple_of_OuterAllowUnknownFields_and_MiddleDenyUnknownFields", + "type": "array", + "prefixItems": [ + { + "$ref": "#/$defs/OuterAllowUnknownFields" + }, + { + "$ref": "#/$defs/MiddleDenyUnknownFields" + } + ], + "minItems": 2, + "maxItems": 2, + "$defs": { + "OuterAllowUnknownFields": { + "type": "object", + "properties": { + "outer_field": { + "type": "boolean" + }, + "middle_field": { + "type": "boolean" + }, + "inner_field": { + "type": "boolean" + } + }, + "required": [ + "outer_field", + "middle_field", + "inner_field" + ] + }, + "MiddleDenyUnknownFields": { + "type": "object", + "properties": { + "middle_field": { + "type": "boolean" + }, + "inner_field": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "middle_field", + "inner_field" + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/transform_enum_external.json b/schemars/tests/expected/transform_enum_external.json new file mode 100644 index 00000000..af746e3c --- /dev/null +++ b/schemars/tests/expected/transform_enum_external.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "External", + "oneOf": [ + { + "type": "string", + "const": "Unit", + "propertyCount": 0, + "upperType": "STRING" + }, + { + "type": "object", + "properties": { + "NewType": true + }, + "required": [ + "NewType" + ], + "additionalProperties": false, + "propertyCount": 1, + "upperType": "OBJECT" + } + ], + "propertyCount": 0 +} \ No newline at end of file diff --git a/schemars/tests/expected/transform_struct.json b/schemars/tests/expected/transform_struct.json new file mode 100644 index 00000000..6723414c --- /dev/null +++ b/schemars/tests/expected/transform_struct.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct", + "type": "object", + "properties": { + "value": true, + "int": { + "type": "integer", + "format": "int32", + "propertyCount": 0, + "upperType": "INTEGER" + } + }, + "required": [ + "value", + "int" + ], + "upperType": "OBJECT", + "propertyCount": 2 +} \ No newline at end of file diff --git a/schemars/tests/expected/transparent-struct.json b/schemars/tests/expected/transparent-struct.json index e83f905b..26920696 100644 --- a/schemars/tests/expected/transparent-struct.json +++ b/schemars/tests/expected/transparent-struct.json @@ -1,12 +1,12 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "OuterStruct", "type": "object", "properties": { "inner": { "anyOf": [ { - "$ref": "#/definitions/InnerStruct" + "$ref": "#/$defs/InnerStruct" }, { "type": "null" @@ -14,10 +14,10 @@ ] } }, - "definitions": { + "$defs": { "InnerStruct": { "type": "array", - "items": [ + "prefixItems": [ { "type": "string" }, @@ -26,8 +26,8 @@ "format": "int32" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } } } \ No newline at end of file diff --git a/schemars/tests/expected/url.json b/schemars/tests/expected/url.json index ddb8c5a1..425bf854 100644 --- a/schemars/tests/expected/url.json +++ b/schemars/tests/expected/url.json @@ -1,14 +1,14 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "UrlTypes", "type": "object", - "required": [ - "url" - ], "properties": { "url": { "type": "string", "format": "uri" } - } -} + }, + "required": [ + "url" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/uuid.json b/schemars/tests/expected/uuid.json index 8ba1a01b..98ae408a 100644 --- a/schemars/tests/expected/uuid.json +++ b/schemars/tests/expected/uuid.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Uuid", "type": "string", "format": "uuid" diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json index d4a14e3f..64b571f0 100644 --- a/schemars/tests/expected/validate.json +++ b/schemars/tests/expected/validate.json @@ -1,37 +1,19 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Struct", "type": "object", - "required": [ - "contains_str1", - "contains_str2", - "email_address", - "homepage", - "map_contains", - "min_max", - "min_max2", - "non_empty_str", - "non_empty_str2", - "pair", - "regex_str1", - "regex_str2", - "regex_str3", - "required_option", - "tel", - "x" - ], "properties": { "min_max": { "type": "number", "format": "float", - "maximum": 100.0, - "minimum": 0.01 + "minimum": 0.01, + "maximum": 100 }, "min_max2": { "type": "number", "format": "float", - "maximum": 1000.0, - "minimum": 1.0 + "minimum": 1, + "maximum": 1000 }, "regex_str1": { "type": "string", @@ -41,10 +23,6 @@ "type": "string", "pattern": "^[Hh]ello\\b" }, - "regex_str3": { - "type": "string", - "pattern": "^\\d+$" - }, "contains_str1": { "type": "string", "pattern": "substring\\.\\.\\." @@ -57,23 +35,19 @@ "type": "string", "format": "email" }, - "tel": { - "type": "string", - "format": "phone" - }, "homepage": { "type": "string", "format": "uri" }, "non_empty_str": { "type": "string", - "maxLength": 100, - "minLength": 1 + "minLength": 1, + "maxLength": 100 }, "non_empty_str2": { "type": "string", - "maxLength": 1000, - "minLength": 1 + "minLength": 1, + "maxLength": 1000 }, "pair": { "type": "array", @@ -81,17 +55,8 @@ "type": "integer", "format": "int32" }, - "maxItems": 2, - "minItems": 2 - }, - "map_contains": { - "type": "object", - "required": [ - "map_key" - ], - "additionalProperties": { - "type": "null" - } + "minItems": 2, + "maxItems": 2 }, "required_option": { "type": "boolean" @@ -100,5 +65,20 @@ "type": "integer", "format": "int32" } - } + }, + "required": [ + "min_max", + "min_max2", + "regex_str1", + "regex_str2", + "contains_str1", + "contains_str2", + "email_address", + "homepage", + "non_empty_str", + "non_empty_str2", + "pair", + "required_option", + "x" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/validate_inner.json b/schemars/tests/expected/validate_inner.json new file mode 100644 index 00000000..a77e0d75 --- /dev/null +++ b/schemars/tests/expected/validate_inner.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct", + "type": "object", + "properties": { + "array_str_length": { + "type": "array", + "items": { + "type": "string", + "minLength": 5, + "maxLength": 100 + }, + "minItems": 2, + "maxItems": 2 + }, + "slice_str_contains": { + "type": "array", + "items": { + "type": "string", + "pattern": "substring\\.\\.\\." + } + }, + "vec_str_regex": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[Hh]ello\\b" + } + }, + "vec_str_length": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + }, + "vec_str_length2": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "minItems": 1, + "maxItems": 3 + }, + "vec_str_url": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "vec_i32_range": { + "type": "array", + "items": { + "type": "integer", + "format": "int32", + "minimum": -10, + "maximum": 10 + } + } + }, + "required": [ + "array_str_length", + "slice_str_contains", + "vec_str_regex", + "vec_str_length", + "vec_str_length2", + "vec_str_url", + "vec_i32_range" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/validate_newtype.json b/schemars/tests/expected/validate_newtype.json index 796aecde..cd835f53 100644 --- a/schemars/tests/expected/validate_newtype.json +++ b/schemars/tests/expected/validate_newtype.json @@ -1,8 +1,8 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "NewType", "type": "integer", "format": "uint8", - "maximum": 10.0, - "minimum": 0.0 + "minimum": 0, + "maximum": 10 } \ 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..1f2e3eb3 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -1,47 +1,25 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Struct", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Struct2", "type": "object", - "required": [ - "contains_str1", - "contains_str2", - "email_address", - "homepage", - "map_contains", - "min_max", - "min_max2", - "non_empty_str", - "non_empty_str2", - "pair", - "regex_str1", - "regex_str2", - "regex_str3", - "required_option", - "tel", - "x" - ], "properties": { "min_max": { "type": "number", "format": "float", - "maximum": 100.0, - "minimum": 0.01 + "minimum": 0.01, + "maximum": 100 }, "min_max2": { "type": "number", "format": "float", - "maximum": 1000.0, - "minimum": 1.0 + "minimum": 1, + "maximum": 1000 }, "regex_str1": { "type": "string", "pattern": "^[Hh]ello\\b" }, "regex_str2": { - "type": "string", - "pattern": "^[Hh]ello\\b" - }, - "regex_str3": { "type": "string", "pattern": "^\\d+$" }, @@ -57,23 +35,19 @@ "type": "string", "format": "email" }, - "tel": { - "type": "string", - "format": "phone" - }, "homepage": { "type": "string", "format": "uri" }, "non_empty_str": { "type": "string", - "maxLength": 100, - "minLength": 1 + "minLength": 1, + "maxLength": 100 }, "non_empty_str2": { "type": "string", - "maxLength": 1000, - "minLength": 1 + "minLength": 1, + "maxLength": 1000 }, "pair": { "type": "array", @@ -81,17 +55,8 @@ "type": "integer", "format": "int32" }, - "maxItems": 2, - "minItems": 2 - }, - "map_contains": { - "type": "object", - "required": [ - "map_key" - ], - "additionalProperties": { - "type": "null" - } + "minItems": 2, + "maxItems": 2 }, "required_option": { "type": "boolean" @@ -100,5 +65,20 @@ "type": "integer", "format": "int32" } - } + }, + "required": [ + "min_max", + "min_max2", + "regex_str1", + "regex_str2", + "contains_str1", + "contains_str2", + "email_address", + "homepage", + "non_empty_str", + "non_empty_str2", + "pair", + "required_option", + "x" + ] } \ No newline at end of file diff --git a/schemars/tests/expected/validate_tuple.json b/schemars/tests/expected/validate_tuple.json index 8ab6eaa6..fa812242 100644 --- a/schemars/tests/expected/validate_tuple.json +++ b/schemars/tests/expected/validate_tuple.json @@ -1,18 +1,18 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Tuple", "type": "array", - "items": [ + "prefixItems": [ { "type": "integer", "format": "uint8", - "maximum": 10.0, - "minimum": 0.0 + "minimum": 0, + "maximum": 10 }, { "type": "boolean" } ], - "maxItems": 2, - "minItems": 2 + "minItems": 2, + "maxItems": 2 } \ No newline at end of file diff --git a/schemars/tests/extend.rs b/schemars/tests/extend.rs new file mode 100644 index 00000000..2b44f5e0 --- /dev/null +++ b/schemars/tests/extend.rs @@ -0,0 +1,96 @@ +mod util; +use schemars::JsonSchema; +use serde_json::Value; +use util::*; + +const THREE: f64 = 3.0; + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(extend("msg" = concat!("hello ", "world"), "obj" = {"array": [null, ()]}))] +#[schemars(extend("3" = THREE), extend("pi" = THREE + 0.14))] +struct Struct { + #[schemars(extend("foo" = "bar"))] + value: Value, + #[schemars(extend("type" = "overridden"))] + int: i32, +} + +#[test] +fn extend_struct() -> TestResult { + test_default_generated_schema::("extend_struct") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(extend("foo" = "bar"))] +enum External { + #[schemars(extend("foo" = "bar"))] + Unit, + #[schemars(extend("foo" = "bar"))] + NewType(Value), + #[schemars(extend("foo" = "bar"))] + Tuple(i32, bool), + #[schemars(extend("foo" = "bar"))] + Struct { i: i32, b: bool }, +} + +#[test] +fn extend_enum_external() -> TestResult { + test_default_generated_schema::("extend_enum_external") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(tag = "typeProperty", extend("foo" = "bar"))] +enum Internal { + #[schemars(extend("foo" = "bar"))] + Unit, + #[schemars(extend("foo" = "bar"))] + NewType(Value), + #[schemars(extend("foo" = "bar"))] + Struct { i: i32, b: bool }, +} + +#[test] +fn extend_enum_internal() -> TestResult { + test_default_generated_schema::("extend_enum_internal") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(untagged, extend("foo" = "bar"))] +enum Untagged { + #[schemars(extend("foo" = "bar"))] + Unit, + #[schemars(extend("foo" = "bar"))] + NewType(Value), + #[schemars(extend("foo" = "bar"))] + Tuple(i32, bool), + #[schemars(extend("foo" = "bar"))] + Struct { i: i32, b: bool }, +} + +#[test] +fn extend_enum_untagged() -> TestResult { + test_default_generated_schema::("extend_enum_untagged") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(tag = "t", content = "c", extend("foo" = "bar"))] +enum Adjacent { + #[schemars(extend("foo" = "bar"))] + Unit, + #[schemars(extend("foo" = "bar"))] + NewType(Value), + #[schemars(extend("foo" = "bar"))] + Tuple(i32, bool), + #[schemars(extend("foo" = "bar"))] + Struct { i: i32, b: bool }, +} + +#[test] +fn extend_enum_adjacent() -> TestResult { + test_default_generated_schema::("extend_enum_adjacent") +} diff --git a/schemars/tests/flatten.rs b/schemars/tests/flatten.rs index dcbf1860..a05a5c0b 100644 --- a/schemars/tests/flatten.rs +++ b/schemars/tests/flatten.rs @@ -1,5 +1,7 @@ mod util; use schemars::JsonSchema; +use serde_json::Value; +use std::collections::BTreeMap; use util::*; #[allow(dead_code)] @@ -53,5 +55,61 @@ fn test_flat_schema() -> TestResult { #[test] fn test_flattened_schema() -> TestResult { + // intentionally using the same file as test_flat_schema, as the schema should be identical test_default_generated_schema::("flatten") } + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct FlattenValue { + flag: bool, + #[serde(flatten)] + value: Value, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename = "FlattenValue")] +struct FlattenMap { + flag: bool, + #[serde(flatten)] + value: BTreeMap, +} + +#[test] +fn test_flattened_value() -> TestResult { + test_default_generated_schema::("flattened_value") +} + +#[test] +fn test_flattened_map() -> TestResult { + // intentionally using the same file as test_flattened_value, as the schema should be identical + test_default_generated_schema::("flattened_value") +} + +#[derive(JsonSchema)] +pub struct OuterAllowUnknownFields { + pub outer_field: bool, + #[serde(flatten)] + pub middle: MiddleDenyUnknownFields, +} + +#[derive(JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct MiddleDenyUnknownFields { + pub middle_field: bool, + #[serde(flatten)] + pub inner: InnerAllowUnknownFields, +} + +#[derive(JsonSchema)] +pub struct InnerAllowUnknownFields { + pub inner_field: bool, +} + +#[test] +fn test_flattened_struct_deny_unknown_fields() -> TestResult { + test_default_generated_schema::<(OuterAllowUnknownFields, MiddleDenyUnknownFields)>( + "test_flattened_struct_deny_unknown_fields", + ) +} diff --git a/schemars/tests/from_value.rs b/schemars/tests/from_value.rs index 3009933b..0934cfd5 100644 --- a/schemars/tests/from_value.rs +++ b/schemars/tests/from_value.rs @@ -1,5 +1,5 @@ mod util; -use schemars::gen::{SchemaGenerator, SchemaSettings}; +use schemars::generate::{SchemaGenerator, SchemaSettings}; use serde::Serialize; use std::collections::HashMap; use util::*; @@ -53,32 +53,32 @@ fn make_value() -> MyStruct { #[test] fn schema_from_value_matches_draft07() -> TestResult { - let gen = SchemaSettings::draft07().into_generator(); - let actual = gen.into_root_schema_for_value(&make_value())?; + let generator = SchemaSettings::draft07().into_generator(); + let actual = generator.into_root_schema_for_value(&make_value())?; test_schema(&actual, "from_value_draft07") } #[test] fn schema_from_value_matches_2019_09() -> TestResult { - let gen = SchemaSettings::draft2019_09().into_generator(); - let actual = gen.into_root_schema_for_value(&make_value())?; + let generator = SchemaSettings::draft2019_09().into_generator(); + let actual = generator.into_root_schema_for_value(&make_value())?; test_schema(&actual, "from_value_2019_09") } #[test] fn schema_from_value_matches_openapi3() -> TestResult { - let gen = SchemaSettings::openapi3().into_generator(); - let actual = gen.into_root_schema_for_value(&make_value())?; + let generator = SchemaSettings::openapi3().into_generator(); + let actual = generator.into_root_schema_for_value(&make_value())?; test_schema(&actual, "from_value_openapi3") } #[test] fn schema_from_json_value() -> TestResult { - let gen = SchemaGenerator::default(); - let actual = gen.into_root_schema_for_value(&serde_json::json!({ + let generator = SchemaGenerator::default(); + let actual = generator.into_root_schema_for_value(&serde_json::json!({ "zero": 0, "one": 1, "minusOne": -1, diff --git a/schemars/tests/garde.rs b/schemars/tests/garde.rs new file mode 100644 index 00000000..8b7ce855 --- /dev/null +++ b/schemars/tests/garde.rs @@ -0,0 +1,99 @@ +mod util; +use schemars::JsonSchema; +use util::*; + +const MIN: u32 = 1; +const MAX: u32 = 1000; + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Struct { + #[garde(range(min = 0.01, max = 100))] + min_max: f32, + #[garde(range(min = MIN, max = MAX))] + min_max2: f32, + #[garde(pattern(r"^[Hh]ello\b"))] + regex_str1: String, + #[garde(contains(concat!("sub","string...")))] + contains_str1: String, + #[garde(email)] + email_address: String, + #[garde(url)] + homepage: String, + #[garde(length(min = 1, max = 100))] + non_empty_str: String, + #[garde(length(min = MIN, max = MAX))] + non_empty_str2: String, + #[garde(length(equal = 2))] + pair: Vec, + #[garde(required)] + required_option: Option, + #[garde(required)] + #[serde(flatten)] + required_flattened: Option, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Inner { + x: i32, +} + +#[test] +fn garde() -> TestResult { + test_default_generated_schema::("garde") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Struct2 { + #[schemars(range(min = 0.01, max = 100))] + min_max: f32, + #[schemars(range(min = MIN, max = MAX))] + min_max2: f32, + #[schemars(pattern(r"^[Hh]ello\b"))] + regex_str1: String, + #[schemars(contains(concat!("sub","string...")))] + contains_str1: String, + #[schemars(email)] + email_address: String, + #[schemars(url)] + homepage: String, + #[schemars(length(min = 1, max = 100))] + non_empty_str: String, + #[schemars(length(min = MIN, max = MAX))] + non_empty_str2: String, + #[schemars(length(equal = 2))] + pair: Vec, + #[schemars(required)] + required_option: Option, + #[schemars(required)] + #[serde(flatten)] + required_flattened: Option, +} + +#[test] +fn garde_schemars_attrs() -> TestResult { + test_default_generated_schema::("garde_schemars_attrs") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Tuple( + #[garde(range(max = 10))] u8, + #[garde(required)] Option, +); + +#[test] +fn garde_tuple() -> TestResult { + test_default_generated_schema::("garde_tuple") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct NewType(#[garde(range(max = 10))] u8); + +#[test] +fn garde_newtype() -> TestResult { + test_default_generated_schema::("garde_newtype") +} diff --git a/schemars/tests/indexmap.rs b/schemars/tests/indexmap.rs index 9501b67c..76fcd560 100644 --- a/schemars/tests/indexmap.rs +++ b/schemars/tests/indexmap.rs @@ -1,13 +1,15 @@ mod util; -use indexmap::{IndexMap, IndexSet}; +use std::collections::hash_map::RandomState; + +use indexmap2::{IndexMap, IndexSet}; use schemars::JsonSchema; use util::*; #[allow(dead_code)] #[derive(JsonSchema)] struct IndexMapTypes { - map: IndexMap, - set: IndexSet, + map: IndexMap, + set: IndexSet, } #[test] diff --git a/schemars/tests/inline_subschemas.rs b/schemars/tests/inline_subschemas.rs index 40bae8fc..a377847f 100644 --- a/schemars/tests/inline_subschemas.rs +++ b/schemars/tests/inline_subschemas.rs @@ -1,5 +1,5 @@ mod util; -use schemars::gen::SchemaSettings; +use schemars::generate::SchemaSettings; use schemars::JsonSchema; use util::*; diff --git a/schemars/tests/no_std.rs b/schemars/tests/no_std.rs new file mode 100644 index 00000000..c011aa29 --- /dev/null +++ b/schemars/tests/no_std.rs @@ -0,0 +1,25 @@ +#![no_std] + +mod util; +use schemars::JsonSchema; +use util::*; + +extern crate alloc as test_alloc; + +#[derive(JsonSchema)] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, +} + +#[derive(JsonSchema)] +pub enum MyEnum { + StringNewType(test_alloc::string::String), + StructVariant { floats: test_alloc::vec::Vec }, +} + +#[test] +fn no_std() -> TestResult { + test_default_generated_schema::("no_std") +} diff --git a/schemars/tests/remote_derive_generic.rs b/schemars/tests/remote_derive_generic.rs index 2a5ac548..53fb7d24 100644 --- a/schemars/tests/remote_derive_generic.rs +++ b/schemars/tests/remote_derive_generic.rs @@ -2,7 +2,7 @@ mod util; use schemars::JsonSchema; use serde::Serialize; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet}; use util::*; #[allow(dead_code)] @@ -20,6 +20,7 @@ enum OrDef { struct Str<'a>(&'a str); +#[allow(dead_code)] #[derive(JsonSchema, Serialize)] #[serde(remote = "Str")] struct StrDef<'a>(&'a str); @@ -38,7 +39,7 @@ struct MyStruct<'a, T: Serialize> { s: Str<'a>, // #[schemars(with = "HashMap::<_, HashSet<_>>")] // map: BTreeMap>, - #[schemars(with = "HashMap::>")] + #[schemars(with = "BTreeMap::>")] fake_map: (), } 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_for_schema.rs b/schemars/tests/schema_for_schema.rs deleted file mode 100644 index d175505c..00000000 --- a/schemars/tests/schema_for_schema.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod util; -use schemars::gen::SchemaSettings; -use schemars::schema::RootSchema; -use util::*; - -#[test] -fn schema_matches_draft07() -> TestResult { - test_generated_schema::("schema", SchemaSettings::draft07()) -} - -#[test] -fn schema_matches_2019_09() -> TestResult { - test_generated_schema::("schema-2019_09", SchemaSettings::draft2019_09()) -} - -#[test] -fn schema_matches_openapi3() -> TestResult { - test_generated_schema::("schema-openapi3", SchemaSettings::openapi3()) -} 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/schema_settings.rs b/schemars/tests/schema_settings.rs new file mode 100644 index 00000000..877a77c3 --- /dev/null +++ b/schemars/tests/schema_settings.rs @@ -0,0 +1,68 @@ +mod util; +use schemars::generate::SchemaSettings; +use schemars::{JsonSchema, Schema}; +use serde_json::Value; +use std::collections::BTreeMap; +use util::*; + +#[derive(JsonSchema)] +pub struct Outer { + #[schemars(example = "eight", example = "null")] + pub int: i32, + pub values: BTreeMap<&'static str, Value>, + pub value: Value, + pub inner: Option, + pub tuples: Vec<(u8, i64)>, +} + +#[derive(JsonSchema)] +pub enum Inner { + UndocumentedUnit1, + UndocumentedUnit2, + /// This is a documented unit variant + DocumentedUnit, + ValueNewType(Value), +} + +fn eight() -> i32 { + 8 +} + +fn null() {} + +#[test] +fn schema_matches_draft07() -> TestResult { + test_generated_schema::("schema_settings", SchemaSettings::draft07()) +} + +#[test] +fn schema_matches_2019_09() -> TestResult { + test_generated_schema::("schema_settings-2019_09", SchemaSettings::draft2019_09()) +} + +#[test] +fn schema_matches_2020_12() -> TestResult { + test_generated_schema::("schema_settings-2020_12", SchemaSettings::draft2020_12()) +} + +#[test] +fn schema_matches_openapi3() -> TestResult { + let mut settings = SchemaSettings::openapi3(); + + // Hack to apply recursive transforms to schemas at components.schemas: + // First, move them to $defs, then run the transforms, then move them back again. + settings.transforms.insert( + 0, + Box::new(|s: &mut Schema| { + let obj = s.ensure_object(); + let defs = obj["components"]["schemas"].take(); + obj.insert("$defs".to_owned(), defs); + }), + ); + settings.transforms.push(Box::new(|s: &mut Schema| { + let obj = s.ensure_object(); + obj["components"]["schemas"] = obj.remove("$defs").unwrap(); + })); + + test_generated_schema::("schema_settings-openapi3", settings) +} diff --git a/schemars/tests/schema_with_enum.rs b/schemars/tests/schema_with_enum.rs index a91aa4d5..df9ab090 100644 --- a/schemars/tests/schema_with_enum.rs +++ b/schemars/tests/schema_with_enum.rs @@ -2,8 +2,8 @@ mod util; use schemars::JsonSchema; use util::*; -fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - ::json_schema(gen) +fn schema_fn(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + ::json_schema(generator) } #[derive(Debug)] diff --git a/schemars/tests/schema_with_struct.rs b/schemars/tests/schema_with_struct.rs index 15fe5a28..86776271 100644 --- a/schemars/tests/schema_with_struct.rs +++ b/schemars/tests/schema_with_struct.rs @@ -2,8 +2,8 @@ mod util; use schemars::JsonSchema; use util::*; -fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - ::json_schema(gen) +fn schema_fn(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + ::json_schema(generator) } struct DoesntImplementJsonSchema; @@ -23,6 +23,7 @@ fn struct_normal() -> TestResult { test_default_generated_schema::("schema_with-struct") } +#[allow(dead_code)] #[derive(JsonSchema)] pub struct Tuple( #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, diff --git a/schemars/tests/semver.rs b/schemars/tests/semver.rs new file mode 100644 index 00000000..3c351c65 --- /dev/null +++ b/schemars/tests/semver.rs @@ -0,0 +1,15 @@ +mod util; +use schemars::JsonSchema; +use semver1::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/skip.rs b/schemars/tests/skip.rs index 8fbfa65a..a95a8db3 100644 --- a/schemars/tests/skip.rs +++ b/schemars/tests/skip.rs @@ -21,6 +21,7 @@ fn skip_struct_fields() -> TestResult { test_default_generated_schema::("skip_struct_fields") } +#[allow(dead_code)] #[derive(JsonSchema)] struct TupleStruct( #[schemars(skip)] i32, diff --git a/schemars/tests/smallvec.rs b/schemars/tests/smallvec.rs index bc9c6ef6..8412a6a2 100644 --- a/schemars/tests/smallvec.rs +++ b/schemars/tests/smallvec.rs @@ -1,5 +1,5 @@ mod util; -use smallvec::SmallVec; +use smallvec1::SmallVec; use util::*; #[test] diff --git a/schemars/tests/smol_str.rs b/schemars/tests/smol_str.rs index 1e481963..43fad304 100644 --- a/schemars/tests/smol_str.rs +++ b/schemars/tests/smol_str.rs @@ -1,5 +1,5 @@ mod util; -use smol_str::SmolStr; +use smol_str02::SmolStr; use util::*; #[test] diff --git a/schemars/tests/time.rs b/schemars/tests/std_time.rs similarity index 100% rename from schemars/tests/time.rs rename to schemars/tests/std_time.rs diff --git a/schemars/tests/struct.rs b/schemars/tests/struct.rs index 108a5d92..80f9ec8d 100644 --- a/schemars/tests/struct.rs +++ b/schemars/tests/struct.rs @@ -18,6 +18,7 @@ fn struct_normal() -> TestResult { test_default_generated_schema::("struct-normal") } +#[allow(dead_code)] #[derive(JsonSchema)] pub struct Tuple(i32, bool, Option<&'static str>); @@ -26,6 +27,7 @@ fn struct_tuple() -> TestResult { test_default_generated_schema::("struct-tuple") } +#[allow(dead_code)] #[derive(JsonSchema)] pub struct Newtype(i32); diff --git a/schemars/tests/transform.rs b/schemars/tests/transform.rs new file mode 100644 index 00000000..23d5b891 --- /dev/null +++ b/schemars/tests/transform.rs @@ -0,0 +1,47 @@ +mod util; +use schemars::{transform::RecursiveTransform, JsonSchema, Schema}; +use serde_json::{Map, Value}; +use util::*; + +fn capitalize_type(schema: &mut Schema) { + if let Some(Value::String(ty)) = schema.get("type") { + schema.insert("upperType".to_owned(), ty.to_uppercase().into()); + } +} + +fn insert_property_count(schema: &mut Schema) { + let count = schema + .get("properties") + .and_then(Value::as_object) + .map_or(0, Map::len); + schema.insert("propertyCount".to_owned(), count.into()); +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)] +struct Struct { + value: Value, + #[schemars(transform = insert_property_count)] + int: i32, +} + +#[test] +fn transform_struct() -> TestResult { + test_default_generated_schema::("transform_struct") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)] +enum External { + #[schemars(transform = insert_property_count)] + Unit, + #[schemars(transform = insert_property_count)] + NewType(Value), +} + +#[test] +fn transform_enum_external() -> TestResult { + test_default_generated_schema::("transform_enum_external") +} diff --git a/schemars/tests/transparent.rs b/schemars/tests/transparent.rs index 2591fb8f..f8b2218e 100644 --- a/schemars/tests/transparent.rs +++ b/schemars/tests/transparent.rs @@ -16,10 +16,12 @@ pub struct TransparentStruct { inner: (), } +#[allow(dead_code)] #[derive(JsonSchema)] #[schemars(transparent)] pub struct TransparentNewType(Option); +#[allow(dead_code)] #[derive(JsonSchema)] pub struct InnerStruct(String, i32); diff --git a/schemars/tests/ui/invalid_extend.rs b/schemars/tests/ui/invalid_extend.rs new file mode 100644 index 00000000..b7295f73 --- /dev/null +++ b/schemars/tests/ui/invalid_extend.rs @@ -0,0 +1,11 @@ +use schemars::JsonSchema; + +#[derive(JsonSchema)] +#[schemars(extend(x))] +#[schemars(extend("x"))] +#[schemars(extend("x" = ))] +#[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))] +#[schemars(extend("y" = "duplicated!"))] +pub struct Struct; + +fn main() {} diff --git a/schemars/tests/ui/invalid_extend.stderr b/schemars/tests/ui/invalid_extend.stderr new file mode 100644 index 00000000..d7d21798 --- /dev/null +++ b/schemars/tests/ui/invalid_extend.stderr @@ -0,0 +1,35 @@ +error: expected string literal + --> tests/ui/invalid_extend.rs:4:19 + | +4 | #[schemars(extend(x))] + | ^ + +error: expected `=` + --> tests/ui/invalid_extend.rs:5:22 + | +5 | #[schemars(extend("x"))] + | ^ + +error: Expected extension value + --> tests/ui/invalid_extend.rs:6:25 + | +6 | #[schemars(extend("x" = ))] + | ^ + +error: Duplicate extension key 'y' + --> tests/ui/invalid_extend.rs:7:32 + | +7 | #[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))] + | ^^^ + +error: Duplicate extension key 'y' + --> tests/ui/invalid_extend.rs:7:61 + | +7 | #[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))] + | ^^^ + +error: Duplicate extension key 'y' + --> tests/ui/invalid_extend.rs:8:19 + | +8 | #[schemars(extend("y" = "duplicated!"))] + | ^^^ diff --git a/schemars/tests/ui/invalid_validation_attrs.rs b/schemars/tests/ui/invalid_validation_attrs.rs index be843625..fef0fe37 100644 --- a/schemars/tests/ui/invalid_validation_attrs.rs +++ b/schemars/tests/ui/invalid_validation_attrs.rs @@ -1,16 +1,19 @@ use schemars::JsonSchema; #[derive(JsonSchema)] -pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String); +#[validate(email)] +pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String); #[derive(JsonSchema)] -pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); +#[schemars(email)] +pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); #[derive(JsonSchema)] pub struct Struct3( #[validate( regex = "foo", contains = "bar", + regex(pattern = "baz"), regex(path = "baz"), phone, email, @@ -25,7 +28,11 @@ pub struct Struct4( regex = "foo", contains = "bar", regex(path = "baz"), + regex(pattern = "baz"), phone, + email(code = "code_str", message = "message"), + email = "foo", + email, email, url )] diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr index 933fd66f..51e75100 100644 --- a/schemars/tests/ui/invalid_validation_attrs.stderr +++ b/schemars/tests/ui/invalid_validation_attrs.stderr @@ -1,59 +1,113 @@ -error: expected validate regex attribute to be a string: `regex = "..."` - --> $DIR/invalid_validation_attrs.rs:4:39 +error: expected validate regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:5:31 | -4 | pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String); - | ^ +5 | pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^ -error: unknown schemars attribute `foo` - --> $DIR/invalid_validation_attrs.rs:7:42 +error: expected schemars regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:9:31 | -7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); - | ^^^ +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^ -error: expected schemars regex attribute to be a string: `regex = "..."` - --> $DIR/invalid_validation_attrs.rs:7:39 +error: schemars attribute cannot contain both `equal` and `min` + --> tests/ui/invalid_validation_attrs.rs:9:59 | -7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); - | ^ +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^^^^^ -error: schemars attribute cannot contain both `equal` and `min` - --> $DIR/invalid_validation_attrs.rs:7:63 +error: unknown item in schemars length attribute: `bar` + --> tests/ui/invalid_validation_attrs.rs:9:70 + | +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^ + +error: unknown schemars attribute `foo` + --> tests/ui/invalid_validation_attrs.rs:9:38 | -7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); - | ^^^^^ +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^ -error: unknown item in schemars length attribute - --> $DIR/invalid_validation_attrs.rs:7:74 +error: unknown schemars attribute `email` + --> tests/ui/invalid_validation_attrs.rs:8:12 | -7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); - | ^^^ +8 | #[schemars(email)] + | ^^^^^ -error: schemars attribute cannot contain both `contains` and `regex` - --> $DIR/invalid_validation_attrs.rs:26:9 +error: expected validate regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:14:9 | -26 | contains = "bar", - | ^^^^^^^^ +14 | regex = "foo", + | ^^^^^^^^^^^^^ -error: duplicate schemars attribute `regex` - --> $DIR/invalid_validation_attrs.rs:27:9 +error: expected validate contains attribute item to be of the form `contains(...)` + --> tests/ui/invalid_validation_attrs.rs:15:9 | -27 | regex(path = "baz"), - | ^^^^^ +15 | contains = "bar", + | ^^^^^^^^^^^^^^^^ + +error: `pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead + --> tests/ui/invalid_validation_attrs.rs:16:15 + | +16 | regex(pattern = "baz"), + | ^^^^^^^^^^^^^^^ + +error: `validate(regex(...))` attribute requires `path = ...` + --> tests/ui/invalid_validation_attrs.rs:16:9 + | +16 | regex(pattern = "baz"), + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: expected schemars regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:28:9 + | +28 | regex = "foo", + | ^^^^^^^^^^^^^ + +error: expected schemars contains attribute item to be of the form `contains(...)` + --> tests/ui/invalid_validation_attrs.rs:29:9 + | +29 | contains = "bar", + | ^^^^^^^^^^^^^^^^ + +error: `path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead + --> tests/ui/invalid_validation_attrs.rs:30:15 + | +30 | regex(path = "baz"), + | ^^^^^^^^^^^^ + +error: `schemars(regex(...))` attribute requires `pattern = ...` + --> tests/ui/invalid_validation_attrs.rs:30:9 + | +30 | regex(path = "baz"), + | ^^^^^^^^^^^^^^^^^^^ + +error: unexpected value of schemars email attribute item + --> tests/ui/invalid_validation_attrs.rs:33:14 + | +33 | email(code = "code_str", message = "message"), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: schemars attribute cannot contain both `phone` and `email` - --> $DIR/invalid_validation_attrs.rs:29:9 +error: unexpected value of schemars email attribute item + --> tests/ui/invalid_validation_attrs.rs:34:15 | -29 | email, +34 | email = "foo", + | ^^^^^^^ + +error: duplicate schemars attribute item `email` + --> tests/ui/invalid_validation_attrs.rs:36:9 + | +36 | email, | ^^^^^ -error: schemars attribute cannot contain both `phone` and `url` - --> $DIR/invalid_validation_attrs.rs:30:9 +error: schemars attribute cannot contain both `url` and `email` + --> tests/ui/invalid_validation_attrs.rs:37:9 | -30 | url +37 | url | ^^^ -error[E0425]: cannot find value `foo` in this scope - --> $DIR/invalid_validation_attrs.rs:12:17 +error: unknown schemars attribute `phone` + --> tests/ui/invalid_validation_attrs.rs:32:9 | -12 | regex = "foo", - | ^^^^^ not found in this scope +32 | phone, + | ^^^^^ diff --git a/schemars/tests/ui/repr_non_unit_variant.stderr b/schemars/tests/ui/repr_non_unit_variant.stderr index eb340391..68afabf4 100644 --- a/schemars/tests/ui/repr_non_unit_variant.stderr +++ b/schemars/tests/ui/repr_non_unit_variant.stderr @@ -2,4 +2,4 @@ error: JsonSchema_repr: must be a unit variant --> $DIR/repr_non_unit_variant.rs:7:5 | 7 | EmptyTuple(), - | ^^^^^^^^^^ + | ^^^^^^^^^^^^ diff --git a/schemars/tests/ui/transform_str.rs b/schemars/tests/ui/transform_str.rs new file mode 100644 index 00000000..6570accf --- /dev/null +++ b/schemars/tests/ui/transform_str.rs @@ -0,0 +1,7 @@ +use schemars::JsonSchema; + +#[derive(JsonSchema)] +#[schemars(transform = "x")] +pub struct Struct; + +fn main() {} diff --git a/schemars/tests/ui/transform_str.stderr b/schemars/tests/ui/transform_str.stderr new file mode 100644 index 00000000..6ee36983 --- /dev/null +++ b/schemars/tests/ui/transform_str.stderr @@ -0,0 +1,6 @@ +error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`. + Did you mean `[schemars(transform = x)]`? + --> tests/ui/transform_str.rs:4:24 + | +4 | #[schemars(transform = "x")] + | ^^^ diff --git a/schemars/tests/url.rs b/schemars/tests/url.rs index 0e9499a2..018a66b5 100644 --- a/schemars/tests/url.rs +++ b/schemars/tests/url.rs @@ -1,6 +1,6 @@ mod util; use schemars::JsonSchema; -use url::Url; +use url2::Url; use util::*; #[allow(dead_code)] diff --git a/schemars/tests/util/mod.rs b/schemars/tests/util/mod.rs index 595d68bf..ff67df65 100644 --- a/schemars/tests/util/mod.rs +++ b/schemars/tests/util/mod.rs @@ -1,7 +1,11 @@ use pretty_assertions::assert_eq; -use schemars::{gen::SchemaSettings, schema::RootSchema, schema_for, JsonSchema}; +use schemars::{generate::SchemaSettings, schema_for, JsonSchema, Schema}; use std::error::Error; +use std::format; use std::fs; +use std::prelude::rust_2021::*; + +extern crate std; pub type TestResult = Result<(), Box>; @@ -17,7 +21,7 @@ pub fn test_default_generated_schema(file: &str) -> TestResult { test_schema(&actual, file) } -pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult { +pub fn test_schema(actual: &Schema, file: &str) -> TestResult { let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) { Ok(j) => j, Err(e) => { @@ -35,7 +39,7 @@ pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult { Ok(()) } -fn write_actual_to_file(schema: &RootSchema, file: &str) -> TestResult { +fn write_actual_to_file(schema: &Schema, file: &str) -> TestResult { let actual_json = serde_json::to_string_pretty(&schema)?; fs::write(format!("tests/actual/{}.json", file), actual_json)?; Ok(()) diff --git a/schemars/tests/uuid.rs b/schemars/tests/uuid.rs index bd673b51..77e92c2c 100644 --- a/schemars/tests/uuid.rs +++ b/schemars/tests/uuid.rs @@ -1,11 +1,6 @@ mod util; use util::*; -#[test] -fn uuid08() -> TestResult { - test_default_generated_schema::("uuid") -} - #[test] fn uuid1() -> TestResult { test_default_generated_schema::("uuid") diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index e3817343..410593d6 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -1,10 +1,17 @@ mod util; use schemars::JsonSchema; -use std::collections::HashMap; use util::*; +struct FakeRegex(&'static str); + +impl std::fmt::Display for FakeRegex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + // In real code, this would typically be a Regex, potentially created in a `lazy_static!`. -static STARTS_WITH_HELLO: &str = r"^[Hh]ello\b"; +static STARTS_WITH_HELLO: &FakeRegex = &FakeRegex(r"^[Hh]ello\b"); const MIN: u32 = 1; const MAX: u32 = 1000; @@ -16,21 +23,17 @@ pub struct Struct { min_max: f32, #[validate(range(min = "MIN", max = "MAX"))] min_max2: f32, - #[validate(regex = "STARTS_WITH_HELLO")] + #[validate(regex(path = *STARTS_WITH_HELLO))] regex_str1: String, #[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))] regex_str2: String, - #[validate(regex(pattern = r"^\d+$"))] - regex_str3: String, - #[validate(contains = "substring...")] + #[validate(contains(pattern = "substring..."))] contains_str1: String, #[validate(contains(pattern = "substring...", message = "bar"))] contains_str2: String, #[validate(email)] email_address: String, - #[validate(phone)] - tel: String, - #[validate(url)] + #[validate(url(code = "code_str", message = "message"))] homepage: String, #[validate(length(min = 1, max = 100))] non_empty_str: String, @@ -38,8 +41,6 @@ pub struct Struct { non_empty_str2: String, #[validate(length(equal = 2))] pair: Vec, - #[validate(contains = "map_key")] - map_contains: HashMap, #[validate(required)] required_option: Option, #[validate(required)] @@ -66,22 +67,18 @@ pub struct Struct2 { min_max: f32, #[schemars(range(min = "MIN", max = "MAX"))] min_max2: f32, - #[validate(regex = "overridden")] - #[schemars(regex = "STARTS_WITH_HELLO")] + #[validate(regex(path = overridden))] + #[schemars(regex(pattern = *STARTS_WITH_HELLO))] regex_str1: String, - #[schemars(regex(path = "STARTS_WITH_HELLO"))] - regex_str2: String, #[schemars(regex(pattern = r"^\d+$"))] - regex_str3: String, - #[validate(regex = "overridden")] - #[schemars(contains = "substring...")] + regex_str2: String, + #[validate(contains(pattern = "overridden"))] + #[schemars(contains(pattern = "substring..."))] contains_str1: String, #[schemars(contains(pattern = "substring..."))] contains_str2: String, #[schemars(email)] email_address: String, - #[schemars(phone)] - tel: String, #[schemars(url)] homepage: String, #[schemars(length(min = 1, max = 100))] @@ -90,8 +87,6 @@ pub struct Struct2 { non_empty_str2: String, #[schemars(length(equal = 2))] pair: Vec, - #[schemars(contains = "map_key")] - map_contains: HashMap, #[schemars(required)] required_option: Option, #[schemars(required)] @@ -101,9 +96,10 @@ pub struct Struct2 { #[test] fn validate_schemars_attrs() -> TestResult { - test_default_generated_schema::("validate_schemars_attrs") + test_default_generated_schema::("validate_schemars_attrs") } +#[allow(dead_code)] #[derive(JsonSchema)] pub struct Tuple( #[validate(range(max = 10))] u8, @@ -115,6 +111,7 @@ fn validate_tuple() -> TestResult { test_default_generated_schema::("validate_tuple") } +#[allow(dead_code)] #[derive(JsonSchema)] pub struct NewType(#[validate(range(max = 10))] u8); diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs new file mode 100644 index 00000000..d3d8d652 --- /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(pattern = 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..1ab7ca98 100644 --- a/schemars_derive/Cargo.toml +++ b/schemars_derive/Cargo.toml @@ -3,12 +3,14 @@ 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 = "1.0.0-alpha.14" authors = ["Graham Esau "] -edition = "2018" +edition = "2021" license = "MIT" readme = "README.md" keywords = ["rust", "json-schema", "serde"] +categories = ["encoding", "no-std"] +rust-version = "1.65" [lib] proc-macro = true @@ -16,8 +18,8 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0", features = ["extra-traits"] } -serde_derive_internals = "0.26.0" +syn = { version = "2.0", features = ["extra-traits"] } +serde_derive_internals = "0.29" [dev-dependencies] pretty_assertions = "1.2.1" diff --git a/schemars_derive/attributes.md b/schemars_derive/attributes.md new file mode 120000 index 00000000..882c89c3 --- /dev/null +++ b/schemars_derive/attributes.md @@ -0,0 +1 @@ +../docs/_includes/attributes.md \ No newline at end of file diff --git a/schemars_derive/deriving.md b/schemars_derive/deriving.md new file mode 120000 index 00000000..075caeee --- /dev/null +++ b/schemars_derive/deriving.md @@ -0,0 +1 @@ +../docs/_includes/deriving.md \ No newline at end of file diff --git a/schemars_derive/docs-rs-custom.css b/schemars_derive/docs-rs-custom.css new file mode 120000 index 00000000..594119fe --- /dev/null +++ b/schemars_derive/docs-rs-custom.css @@ -0,0 +1 @@ +../docs-rs-custom.css \ No newline at end of file diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index 453c247f..d9ea0667 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -1,5 +1,4 @@ use super::*; -use crate::attr::Attrs; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::Ctxt; @@ -26,9 +25,7 @@ impl<'a> FromSerde for Container<'a> { serde_attrs: serde.attrs, data: Data::from_serde(errors, serde.data)?, generics: serde.generics.clone(), - original: serde.original, - // FIXME this allows with/schema_with attribute on containers - attrs: Attrs::new(&serde.original.attrs, errors), + attrs: ContainerAttrs::new(&serde.original.attrs, errors), }) } } @@ -58,7 +55,7 @@ impl<'a> FromSerde for Variant<'a> { style: serde.style, fields: Field::vec_from_serde(errors, serde.fields)?, original: serde.original, - attrs: Attrs::new(&serde.original.attrs, errors), + attrs: VariantAttrs::new(&serde.original.attrs, errors), }) } } @@ -72,8 +69,7 @@ impl<'a> FromSerde for Field<'a> { serde_attrs: serde.attrs, ty: serde.ty, original: serde.original, - attrs: Attrs::new(&serde.original.attrs, errors), - validation_attrs: ValidationAttrs::new(&serde.original.attrs, errors), + attrs: FieldAttrs::new(&serde.original.attrs, errors), }) } } diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 7b9052e4..0b29ca1e 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -1,7 +1,9 @@ mod from_serde; -use crate::attr::{Attrs, ValidationAttrs}; +use crate::attr::{ContainerAttrs, FieldAttrs, VariantAttrs}; +use crate::idents::SCHEMA; use from_serde::FromSerde; +use proc_macro2::TokenStream; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::{Ctxt, Derive}; @@ -10,8 +12,7 @@ pub struct Container<'a> { pub serde_attrs: serde_derive_internals::attr::Container, pub data: Data<'a>, pub generics: syn::Generics, - pub original: &'a syn::DeriveInput, - pub attrs: Attrs, + pub attrs: ContainerAttrs, } pub enum Data<'a> { @@ -25,7 +26,7 @@ pub struct Variant<'a> { pub style: serde_ast::Style, pub fields: Vec>, pub original: &'a syn::Variant, - pub attrs: Attrs, + pub attrs: VariantAttrs, } pub struct Field<'a> { @@ -33,12 +34,11 @@ pub struct Field<'a> { pub serde_attrs: serde_derive_internals::attr::Field, pub ty: &'a syn::Type, pub original: &'a syn::Field, - pub attrs: Attrs, - pub validation_attrs: ValidationAttrs, + pub attrs: FieldAttrs, } impl<'a> Container<'a> { - pub fn from_ast(item: &'a syn::DeriveInput) -> Result, Vec> { + pub fn from_ast(item: &'a syn::DeriveInput) -> syn::Result> { let ctxt = Ctxt::new(); let result = serde_ast::Container::from_ast(&ctxt, item, Derive::Deserialize) .ok_or(()) @@ -48,7 +48,7 @@ impl<'a> Container<'a> { .map(|_| result.expect("from_ast set no errors on Ctxt, so should have returned Ok")) } - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } @@ -61,23 +61,44 @@ impl<'a> Container<'a> { None } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); + } } impl<'a> Variant<'a> { - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } pub fn is_unit(&self) -> bool { - match self.style { - serde_ast::Style::Unit => true, - _ => false, - } + matches!(self.style, serde_ast::Style::Unit) + } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); } } impl<'a> Field<'a> { - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); + self.attrs.validation.add_mutators(mutators); + + if self.serde_attrs.skip_deserializing() { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "readOnly", true); + }); + } + if self.serde_attrs.skip_serializing() { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "writeOnly", true); + }); + } + } } diff --git a/schemars_derive/src/attr/doc.rs b/schemars_derive/src/attr/doc.rs index 0827dc01..778d493a 100644 --- a/schemars_derive/src/attr/doc.rs +++ b/schemars_derive/src/attr/doc.rs @@ -1,82 +1,20 @@ -use syn::{Attribute, Lit::Str, Meta::NameValue, MetaNameValue}; +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use syn::Attribute; -pub fn get_title_and_desc_from_doc(attrs: &[Attribute]) -> (Option, Option) { - let doc = match get_doc(attrs) { - None => return (None, None), - Some(doc) => doc, - }; +pub fn get_doc(attrs: &[Attribute]) -> Option { + let mut macro_args: TokenStream = TokenStream::new(); - if doc.starts_with('#') { - let mut split = doc.splitn(2, '\n'); - let title = split - .next() - .unwrap() - .trim_start_matches('#') - .trim() - .to_owned(); - let maybe_desc = split.next().and_then(merge_description_lines); - (none_if_empty(title), maybe_desc) - } else { - (None, merge_description_lines(&doc)) - } -} - -fn merge_description_lines(doc: &str) -> Option { - let desc = doc - .trim() - .split("\n\n") - .filter_map(|line| none_if_empty(line.trim().replace('\n', " "))) - .collect::>() - .join("\n\n"); - none_if_empty(desc) -} - -fn get_doc(attrs: &[Attribute]) -> Option { - let attrs = attrs + let lines = attrs .iter() - .filter_map(|attr| { - if !attr.path.is_ident("doc") { - return None; - } - - let meta = attr.parse_meta().ok()?; - if let NameValue(MetaNameValue { lit: Str(s), .. }) = meta { - return Some(s.value()); - } - - None - }) - .collect::>(); - - let mut lines = attrs - .iter() - .flat_map(|a| a.split('\n')) - .map(str::trim) - .skip_while(|s| s.is_empty()) - .collect::>(); - - if let Some(&"") = lines.last() { - lines.pop(); - } - - // Added for backward-compatibility, but perhaps we shouldn't do this - // https://github.com/rust-lang/rust/issues/32088 - if lines.iter().all(|l| l.starts_with('*')) { - for line in lines.iter_mut() { - *line = line[1..].trim() - } - while let Some(&"") = lines.first() { - lines.remove(0); - } - }; - - none_if_empty(lines.join("\n")) -} + .filter(|a| a.path().is_ident("doc")) + .flat_map(|a| a.meta.require_name_value()) + .map(|m| &m.value); + macro_args.append_separated(lines, quote!(, "\n",)); -fn none_if_empty(s: String) -> Option { - if s.is_empty() { + if macro_args.is_empty() { None } else { - Some(s) + Some(parse_quote!(::core::concat!(#macro_args))) } } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index f790694a..14fd6eb7 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -1,332 +1,457 @@ mod doc; +mod parse_meta; mod schemars_to_serde; mod validation; -pub use schemars_to_serde::process_serde_attrs; -pub use validation::ValidationAttrs; - -use crate::metadata::SchemaMetadata; -use proc_macro2::{Group, Span, TokenStream, TokenTree}; +use parse_meta::{parse_extensions, parse_name_value_expr, parse_name_value_lit_str}; +use proc_macro2::TokenStream; use quote::ToTokens; use serde_derive_internals::Ctxt; -use syn::parse::{self, Parse}; -use syn::Meta::{List, NameValue}; -use syn::MetaNameValue; -use syn::NestedMeta::{Lit, Meta}; +use syn::Ident; +use syn::{punctuated::Punctuated, Attribute, Expr, ExprLit, Lit, Meta, Path, Type}; +use validation::ValidationAttrs; + +use crate::idents::SCHEMA; -// FIXME using the same struct for containers+variants+fields means that -// with/schema_with are accepted (but ignored) on containers, and -// repr/crate_name are accepted (but ignored) on variants and fields etc. +pub use schemars_to_serde::process_serde_attrs; #[derive(Debug, Default)] -pub struct Attrs { - pub with: Option, - pub title: Option, - pub description: Option, +pub struct CommonAttrs { + pub doc: Option, pub deprecated: bool, - pub examples: Vec, - pub repr: Option, - pub crate_name: Option, + pub title: Option, + pub description: Option, + pub examples: Vec, + pub extensions: Vec<(String, TokenStream)>, + pub transforms: Vec, +} + +#[derive(Debug, Default)] +pub struct FieldAttrs { + pub common: CommonAttrs, + pub with: Option, + pub validation: ValidationAttrs, +} + +#[derive(Debug, Default)] +pub struct ContainerAttrs { + pub common: CommonAttrs, + pub repr: Option, + pub crate_name: Option, pub is_renamed: bool, } +#[derive(Debug, Default)] +pub struct VariantAttrs { + pub common: CommonAttrs, + pub with: Option, +} + #[derive(Debug)] pub enum WithAttr { - Type(syn::Type), - Function(syn::Path), + Type(Type), + Function(Path), } -impl Attrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - let mut result = Attrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "serde", true, errors); - - result.deprecated = attrs.iter().any(|a| a.path.is_ident("deprecated")); - result.repr = attrs - .iter() - .find(|a| a.path.is_ident("repr")) - .and_then(|a| a.parse_args().ok()); - - let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs); - result.title = result.title.or(doc_title); - result.description = result.description.or(doc_description); +impl CommonAttrs { + fn populate( + &mut self, + attrs: &[Attribute], + schemars_cx: &mut AttrCtxt, + serde_cx: &mut AttrCtxt, + ) { + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + + self.doc = doc::get_doc(attrs); + self.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated")); + } - result + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); } - pub fn as_metadata(&self) -> SchemaMetadata<'_> { - #[allow(clippy::ptr_arg)] - fn none_if_empty(s: &String) -> Option<&str> { - if s.is_empty() { - None - } else { - Some(s) - } - } + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "title" => match self.title { + Some(_) => cx.duplicate_error(&meta), + None => self.title = parse_name_value_expr(meta, cx).ok(), + }, - SchemaMetadata { - title: self.title.as_ref().and_then(none_if_empty), - description: self.description.as_ref().and_then(none_if_empty), - deprecated: self.deprecated, - examples: &self.examples, - read_only: false, - write_only: false, - default: None, - } - } + "description" => match self.description { + Some(_) => cx.duplicate_error(&meta), + None => self.description = parse_name_value_expr(meta, cx).ok(), + }, - fn populate( - mut self, - attrs: &[syn::Attribute], - attr_type: &'static str, - ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |meta: &MetaNameValue| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - meta.path.get_ident().unwrap() - ); - errors.error_spanned_by(meta, msg) - } - }; - let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - meta.path.get_ident().unwrap(), - other, - ); - errors.error_spanned_by(meta, msg) + "example" => { + self.examples.extend(parse_name_value_lit_str(meta, cx)); } - }; - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { - 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) { - match self.with { - Some(WithAttr::Type(_)) => duplicate_error(m), - Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"), - None => self.with = Some(WithAttr::Type(ty)), - } + "extend" => { + for ex in parse_extensions(meta, cx).into_iter().flatten() { + // This is O(n^2) but should be fine with the typically small number of + // extensions. If this does become a problem, it can be changed to use + // IndexMap, or a separate Map with cloned keys. + if self.extensions.iter().any(|e| e.0 == ex.key_str) { + cx.error_spanned_by( + ex.key_lit, + format_args!("Duplicate extension key '{}'", ex.key_str), + ); + } else { + self.extensions.push((ex.key_str, ex.value)); } } + } - Meta(NameValue(m)) if m.path.is_ident("schema_with") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.lit) { - match self.with { - Some(WithAttr::Function(_)) => duplicate_error(m), - Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"), - None => self.with = Some(WithAttr::Function(fun)), + "transform" => { + if let Ok(expr) = parse_name_value_expr(meta, cx) { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = &expr + { + if lit_str.parse::().is_ok() { + cx.error_spanned_by( + &expr, + format_args!( + "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?", + lit_str.value() + ), + ) } + } else { + self.transforms.push(expr); } } + } - Meta(NameValue(m)) if m.path.is_ident("title") => { - if let Ok(title) = get_lit_str(errors, attr_type, "title", &m.lit) { - match self.title { - Some(_) => duplicate_error(m), - None => self.title = Some(title.value()), - } - } - } + _ => return Some(meta), + } - Meta(NameValue(m)) if m.path.is_ident("description") => { - if let Ok(description) = get_lit_str(errors, attr_type, "description", &m.lit) { - match self.description { - Some(_) => duplicate_error(m), - None => self.description = Some(description.value()), - } - } - } + None + } - Meta(NameValue(m)) if m.path.is_ident("example") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.lit) { - self.examples.push(fun) - } - } + fn is_default(&self) -> bool { + matches!( + self, + Self { + title: None, + description: None, + doc: None, + deprecated: false, + examples, + extensions, + transforms, + } if examples.is_empty() && extensions.is_empty() && transforms.is_empty() + ) + } - Meta(NameValue(m)) if m.path.is_ident("rename") => self.is_renamed = true, + pub fn add_mutators(&self, mutators: &mut Vec) { + let mut title = self.title.as_ref().map(ToTokens::to_token_stream); + let mut description = self.description.as_ref().map(ToTokens::to_token_stream); + if let Some(doc) = &self.doc { + if title.is_none() || description.is_none() { + mutators.push(quote!{ + const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc); + }); + title.get_or_insert_with(|| quote!(title_and_description.0)); + description.get_or_insert_with(|| quote!(title_and_description.1)); + } + } + if let Some(title) = title { + mutators.push(quote! { + schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "title", #title); + }); + } + if let Some(description) = description { + mutators.push(quote! { + schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "description", #description); + }); + } - Meta(NameValue(m)) if m.path.is_ident("crate") && attr_type == "schemars" => { - if let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.lit) { - if self.crate_name.is_some() { - duplicate_error(m) - } else { - self.crate_name = Some(p) - } - } - } + if self.deprecated { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "deprecated", true); + }); + } - _ if ignore_errors => {} - - Meta(meta_item) => { - if !is_known_serde_or_validation_keyword(meta_item) { - let path = meta_item - .path() - .into_token_stream() - .to_string() - .replace(' ', ""); - errors.error_spanned_by( - meta_item.path(), - format!("unknown schemars attribute `{}`", path), - ); - } + if !self.examples.is_empty() { + let examples = self.examples.iter().map(|eg| { + quote! { + schemars::_private::serde_json::value::to_value(#eg()) } + }); + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "examples", schemars::_private::serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect())); + }); + } - Lit(lit) => { - errors.error_spanned_by(lit, "unexpected literal in schemars attribute"); - } - } + for (k, v) in &self.extensions { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, #k, schemars::_private::serde_json::json!(#v)); + }); } - self - } - pub fn is_default(&self) -> bool { - match self { - Self { - with: None, - title: None, - description: None, - deprecated: false, - examples, - repr: None, - crate_name: None, - is_renamed: _, - } if examples.is_empty() => true, - _ => false, + for transform in &self.transforms { + mutators.push(quote! { + schemars::transform::Transform::transform(&mut #transform, &mut #SCHEMA); + }); } } } -fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool { - let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS - .iter() - .chain(validation::VALIDATION_KEYWORDS); - meta.path() - .get_ident() - .map(|i| known_keywords.any(|k| i == k)) - .unwrap_or(false) -} +impl FieldAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } -fn get_meta_items( - attr: &syn::Attribute, - attr_type: &'static str, - errors: &Ctxt, - ignore_errors: bool, -) -> Result, ()> { - if !attr.path.is_ident(attr_type) { - return Ok(Vec::new()); + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + let validate_cx = &mut AttrCtxt::new(cx, attrs, "validate"); + let garde_cx = &mut AttrCtxt::new(cx, attrs, "garde"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.validation.populate(schemars_cx, validate_cx, garde_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); } - 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(()) - } - Err(err) => { - if !ignore_errors { - errors.error_spanned_by(attr, err) - } - Err(()) + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "with" => match self.with { + Some(WithAttr::Type(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"), + None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type), + }, + "schema_with" if cx.attr_type == "schemars" => match self.with { + Some(WithAttr::Function(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"), + None => { + self.with = parse_name_value_lit_str(meta, cx) + .ok() + .map(WithAttr::Function) + } + }, + + _ => return Some(meta), } + + None } } -fn get_lit_str<'a>( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &'a syn::Lit, -) -> Result<&'a syn::LitStr, ()> { - if let syn::Lit::Str(lit) = lit { - Ok(lit) - } else { - cx.error_spanned_by( - lit, - format!( - "expected {} {} attribute to be a string: `{} = \"...\"`", - attr_type, meta_item_name, meta_item_name - ), - ); - Err(()) +impl ContainerAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } + + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + + self.repr = attrs + .iter() + .find(|a| a.path().is_ident("repr")) + .and_then(|a| a.parse_args().ok()); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "crate" => match self.crate_name { + Some(_) => cx.duplicate_error(&meta), + None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(), + }, + + // The actual parsing of `rename` is done by serde + "rename" => self.is_renamed = true, + + _ => return Some(meta), + }; + + None } } -fn parse_lit_into_ty( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; - - parse_lit_str(string).map_err(|_| { - cx.error_spanned_by( - lit, - format!( - "failed to parse type: `{} = {:?}`", - meta_item_name, - string.value() - ), +impl VariantAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } + + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "with" => match self.with { + Some(WithAttr::Type(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"), + None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type), + }, + "schema_with" if cx.attr_type == "schemars" => match self.with { + Some(WithAttr::Function(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"), + None => { + self.with = parse_name_value_lit_str(meta, cx) + .ok() + .map(WithAttr::Function) + } + }, + + _ => return Some(meta), + } + + None + } + + pub fn is_default(&self) -> bool { + matches!( + self, + Self { + common, + with: None, + } if common.is_default() ) - }) + } } -fn parse_lit_into_path( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; - - parse_lit_str(string).map_err(|_| { - cx.error_spanned_by( - lit, - format!( - "failed to parse path: `{} = {:?}`", - meta_item_name, - string.value() - ), - ) - }) +fn get_meta_items(attrs: &[Attribute], attr_type: &'static str, cx: &Ctxt) -> Vec { + let mut result = vec![]; + + for attr in attrs.iter().filter(|a| a.path().is_ident(attr_type)) { + match attr.parse_args_with(Punctuated::::parse_terminated) { + Ok(list) => result.extend(list), + Err(err) => { + if attr_type == "schemars" { + cx.syn_error(err) + } + } + } + } + + result } -fn parse_lit_str(s: &syn::LitStr) -> parse::Result -where - T: Parse, -{ - let tokens = spanned_tokens(s)?; - syn::parse2(tokens) +fn path_str(path: &Path) -> String { + path.get_ident() + .map(Ident::to_string) + .unwrap_or_else(|| path.into_token_stream().to_string().replace(' ', "")) } -fn spanned_tokens(s: &syn::LitStr) -> parse::Result { - let stream = syn::parse_str(&s.value())?; - Ok(respan_token_stream(stream, s.span())) +pub struct AttrCtxt<'a> { + inner: &'a Ctxt, + attr_type: &'static str, + metas: Vec, } -fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream { - stream - .into_iter() - .map(|token| respan_token_tree(token, span)) - .collect() +impl<'a> AttrCtxt<'a> { + pub fn new(inner: &'a Ctxt, attrs: &'a [Attribute], attr_type: &'static str) -> Self { + Self { + inner, + attr_type, + metas: get_meta_items(attrs, attr_type, inner), + } + } + + pub fn new_nested_meta(&self, metas: Vec) -> Self { + Self { metas, ..*self } + } + + pub fn parse_meta(&mut self, mut handle: impl FnMut(Meta, &str, &Self) -> Option) { + let metas = std::mem::take(&mut self.metas); + self.metas = metas + .into_iter() + .filter_map(|meta| { + meta.path() + .get_ident() + .map(Ident::to_string) + .and_then(|name| handle(meta, &name, self)) + }) + .collect(); + } + + pub fn error_spanned_by(&self, obj: A, msg: T) { + self.inner.error_spanned_by(obj, msg); + } + + pub fn syn_error(&self, err: syn::Error) { + self.inner.syn_error(err); + } + + pub fn mutual_exclusive_error(&self, meta: &Meta, other_attr: &str) { + if self.attr_type == "schemars" { + self.error_spanned_by( + meta, + format_args!( + "schemars attribute cannot contain both `{}` and `{}`", + path_str(meta.path()), + other_attr, + ), + ); + } + } + + pub fn duplicate_error(&self, meta: &Meta) { + if self.attr_type == "schemars" { + self.error_spanned_by( + meta, + format_args!( + "duplicate schemars attribute item `{}`", + path_str(meta.path()) + ), + ); + } + } } -fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree { - if let TokenTree::Group(g) = &mut token { - *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span)); +impl Drop for AttrCtxt<'_> { + fn drop(&mut self) { + if self.attr_type == "schemars" { + for unhandled_meta in self.metas.iter().filter(|m| !is_known_serde_keyword(m)) { + self.error_spanned_by( + unhandled_meta.path(), + format_args!( + "unknown schemars attribute `{}`", + path_str(unhandled_meta.path()) + ), + ); + } + } } - token.set_span(span); - token +} + +fn is_known_serde_keyword(meta: &Meta) -> bool { + let known_keywords = schemars_to_serde::SERDE_KEYWORDS; + meta.path() + .get_ident() + .map(|i| known_keywords.contains(&i.to_string().as_str())) + .unwrap_or(false) } diff --git a/schemars_derive/src/attr/parse_meta.rs b/schemars_derive/src/attr/parse_meta.rs new file mode 100644 index 00000000..94c89eee --- /dev/null +++ b/schemars_derive/src/attr/parse_meta.rs @@ -0,0 +1,344 @@ +use proc_macro2::{TokenStream, TokenTree}; +use syn::{ + parse::{Parse, ParseStream, Parser}, + punctuated::Punctuated, + Expr, ExprLit, Lit, LitStr, Meta, MetaList, MetaNameValue, +}; + +use super::{path_str, AttrCtxt}; + +pub fn require_path_only(meta: Meta, cx: &AttrCtxt) -> Result<(), ()> { + match meta { + Meta::Path(_) => Ok(()), + Meta::List(MetaList { + path, delimiter, .. + }) => { + let name = path_str(&path); + cx.syn_error(syn::Error::new( + delimiter.span().join(), + format_args!( + "unexpected value of {} {} attribute item", + cx.attr_type, name + ), + )); + Err(()) + } + Meta::NameValue(MetaNameValue { + path, + eq_token, + value, + }) => { + let name = path_str(&path); + cx.error_spanned_by( + quote!(#eq_token #value), + format_args!( + "unexpected value of {} {} attribute item", + cx.attr_type, name + ), + ); + Err(()) + } + } +} + +pub fn parse_name_value_expr(meta: Meta, cx: &AttrCtxt) -> Result { + match meta { + Meta::NameValue(m) => Ok(m.value), + _ => { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to have a value: `{} = ...`", + cx.attr_type, name, name + ), + ); + Err(()) + } + } +} + +pub fn parse_name_value_lit_str(meta: Meta, cx: &AttrCtxt) -> Result { + let Meta::NameValue(MetaNameValue { + value: Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }), + .. + }) = meta + else { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to have a string value: `{} = \"...\"`", + cx.attr_type, name, name + ), + ); + return Err(()); + }; + + parse_lit_str(lit_str, cx) +} + +fn parse_lit_str(lit_str: LitStr, cx: &AttrCtxt) -> Result { + lit_str.parse().map_err(|_| { + cx.error_spanned_by( + &lit_str, + format_args!( + "failed to parse \"{}\" as a {}", + lit_str.value(), + std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or_default() + .to_ascii_lowercase(), + ), + ); + }) +} + +pub fn parse_extensions( + meta: Meta, + cx: &AttrCtxt, +) -> Result, ()> { + let parser = Punctuated::::parse_terminated; + parse_meta_list_with(&meta, cx, parser) +} + +pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let outer_name = path_str(outer_meta.path()); + let mut result = LengthOrRange::default(); + + for nested_meta in parse_nested_meta(outer_meta, cx)? { + match path_str(nested_meta.path()).as_str() { + "min" => match (&result.min, &result.equal) { + (Some(_), _) => cx.duplicate_error(&nested_meta), + (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"), + _ => result.min = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "max" => match (&result.max, &result.equal) { + (Some(_), _) => cx.duplicate_error(&nested_meta), + (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"), + _ => result.max = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "equal" => match (&result.min, &result.max, &result.equal) { + (Some(_), _, _) => cx.mutual_exclusive_error(&nested_meta, "min"), + (_, Some(_), _) => cx.mutual_exclusive_error(&nested_meta, "max"), + (_, _, Some(_)) => cx.duplicate_error(&nested_meta), + _ => result.equal = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + unknown => { + if cx.attr_type == "schemars" { + cx.error_spanned_by( + nested_meta, + format_args!( + "unknown item in schemars {outer_name} attribute: `{unknown}`", + ), + ); + } + } + } + } + + Ok(result) +} + +pub fn parse_pattern(meta: Meta, cx: &AttrCtxt) -> Result { + parse_meta_list_with(&meta, cx, Expr::parse) +} + +pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let mut pattern = None; + + for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? { + match path_str(nested_meta.path()).as_str() { + "pattern" => match &pattern { + Some(_) => cx.duplicate_error(&nested_meta), + None => pattern = parse_name_value_expr(nested_meta, cx).ok(), + }, + "path" => { + cx.error_spanned_by(nested_meta, "`path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead") + }, + unknown => { + cx.error_spanned_by( + nested_meta, + format_args!("unknown item in schemars `regex` attribute: `{unknown}`"), + ); + } + } + } + + pattern.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`schemars(regex(...))` attribute requires `pattern = ...`", + ) + }) +} + +pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let mut path = None; + + for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? { + match path_str(nested_meta.path()).as_str() { + "path" => match &path{ + Some(_) => cx.duplicate_error(&nested_meta), + None => path = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "pattern" => { + cx.error_spanned_by(nested_meta, "`pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead") + }, + _ => { + // ignore unknown properties in `validate` attribute + } + } + } + + path.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`validate(regex(...))` attribute requires `path = ...`", + ) + }) +} + +pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result { + #[derive(Debug)] + enum ContainsFormat { + Metas(Punctuated), + Expr(Expr), + } + + impl Parse for ContainsFormat { + fn parse(input: ParseStream) -> syn::Result { + // An imperfect but good-enough heuristic for determining whether it looks more like a + // comma-separated meta list (validator-style), or a single expression (garde-style). + // This heuristic may not generalise well-enough for attributes other than `contains`! + // `foo = bar` => Metas (not Expr::Assign) + // `foo, bar` => Metas + // `foo` => Expr (not Meta::Path) + // `foo(bar)` => Expr (not Meta::List) + if input.peek2(Token![,]) || input.peek2(Token![=]) { + Punctuated::parse_terminated(input).map(Self::Metas) + } else { + input.parse().map(Self::Expr) + } + } + } + + let nested_meta_or_expr = match cx.attr_type { + "validate" => parse_meta_list_with(&outer_meta, cx, Punctuated::parse_terminated) + .map(ContainsFormat::Metas), + "garde" => parse_meta_list_with(&outer_meta, cx, Expr::parse).map(ContainsFormat::Expr), + "schemars" => parse_meta_list_with(&outer_meta, cx, ContainsFormat::parse), + wat => { + unreachable!("Unexpected attr type `{wat}` for `contains` item. This is a bug in schemars, please raise an issue!") + } + }?; + + let nested_metas = match nested_meta_or_expr { + ContainsFormat::Expr(expr) => return Ok(expr), + ContainsFormat::Metas(m) => m, + }; + + let mut pattern = None; + + for nested_meta in nested_metas { + match path_str(nested_meta.path()).as_str() { + "pattern" => match &pattern { + Some(_) => cx.duplicate_error(&nested_meta), + None => pattern = parse_name_value_expr(nested_meta, cx).ok(), + }, + unknown => { + if cx.attr_type == "schemars" { + cx.error_spanned_by( + nested_meta, + format_args!("unknown item in schemars `contains` attribute: `{unknown}`"), + ); + } + } + } + } + + pattern.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`contains` attribute item requires `pattern = ...`", + ) + }) +} + +pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result, ()> { + let parser = Punctuated::::parse_terminated; + parse_meta_list_with(&meta, cx, parser) +} + +fn parse_meta_list_with(meta: &Meta, cx: &AttrCtxt, parser: F) -> Result { + let Meta::List(meta_list) = meta else { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to be of the form `{}(...)`", + cx.attr_type, name, name + ), + ); + return Err(()); + }; + + meta_list.parse_args_with(parser).map_err(|err| { + cx.syn_error(err); + }) +} + +// Like `parse_name_value_expr`, but if the result is a string literal, then parse its contents. +pub fn parse_name_value_expr_handle_lit_str(meta: Meta, cx: &AttrCtxt) -> Result { + let expr = parse_name_value_expr(meta, cx)?; + + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = expr + { + parse_lit_str(lit_str, cx) + } else { + Ok(expr) + } +} + +#[derive(Debug, Default)] +pub struct LengthOrRange { + pub min: Option, + pub max: Option, + pub equal: Option, +} + +#[derive(Debug)] +pub struct Extension { + pub key_str: String, + pub key_lit: LitStr, + pub value: TokenStream, +} + +impl Parse for Extension { + fn parse(input: ParseStream) -> syn::Result { + let key = input.parse::()?; + input.parse::()?; + let mut value = TokenStream::new(); + + while !input.is_empty() && !input.peek(Token![,]) { + value.extend([input.parse::()?]); + } + + if value.is_empty() { + return Err(syn::Error::new(input.span(), "Expected extension value")); + } + + Ok(Extension { + key_str: key.value(), + key_lit: key, + value, + }) + } +} diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 7878a2c1..bc43fee0 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -2,12 +2,16 @@ use quote::ToTokens; use serde_derive_internals::Ctxt; use std::collections::HashSet; use syn::parse::Parser; -use syn::{Attribute, Data, Field, Meta, NestedMeta, Variant}; +use syn::{Attribute, Data, Field, Meta, Variant}; -// List of keywords that can appear in #[serde(...)]/#[schemars(...)] attributes which we want serde_derive_internals to parse for us. +use super::get_meta_items; + +// List of keywords that can appear in #[serde(...)]/#[schemars(...)] attributes which we want +// serde_derive_internals to parse for us. pub(crate) static SERDE_KEYWORDS: &[&str] = &[ "rename", "rename_all", + "rename_all_fields", "deny_unknown_fields", "tag", "content", @@ -20,19 +24,22 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[ "flatten", "remote", "transparent", - // Special case - `bound` is removed from serde attrs, so is only respected when present in schemars attr. + // Special case - `bound` is removed from serde attrs, so is only respected when present in + // schemars attr. "bound", - // Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars attrs to serde attrs. - // This is because we want to preserve any serde attribute's `serialize_with` value to determine whether the field's - // default value should be serialized. We also check the `with` value on schemars/serde attrs e.g. to support deriving - // JsonSchema on remote types, but we parse that ourselves rather than using serde_derive_internals. + // Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars + // attrs to serde attrs. This is because we want to preserve any serde attribute's + // `serialize_with` value to determine whether the field's default value should be + // serialized. We also check the `with` value on schemars/serde attrs e.g. to support deriving + // JsonSchema on remote types, but we parse that ourselves rather than using + // serde_derive_internals. "serialize_with", "with", ]; // If a struct/variant/field has any #[schemars] attributes, then create copies of them // as #[serde] attributes so that serde_derive_internals will parse them for us. -pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> Result<(), Vec> { +pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> syn::Result<()> { let ctxt = Ctxt::new(); process_attrs(&ctxt, &mut input.attrs); match input.data { @@ -60,28 +67,22 @@ fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator) { // Remove #[serde(...)] attributes (some may be re-added later) let (serde_attrs, other_attrs): (Vec<_>, Vec<_>) = - attrs.drain(..).partition(|at| at.path.is_ident("serde")); + attrs.drain(..).partition(|at| at.path().is_ident("serde")); *attrs = other_attrs; - let schemars_attrs: Vec<_> = attrs - .iter() - .filter(|at| at.path.is_ident("schemars")) - .collect(); - // Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes - let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = schemars_attrs - .iter() - .flat_map(|at| get_meta_items(ctxt, at)) - .flatten() - .filter_map(|meta| { - let keyword = get_meta_ident(ctxt, &meta).ok()?; - if keyword.ends_with("with") || !SERDE_KEYWORDS.contains(&keyword.as_ref()) { - None - } else { - Some((meta, keyword)) - } - }) - .unzip(); + let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = + get_meta_items(attrs, "schemars", ctxt) + .into_iter() + .filter_map(|meta| { + let keyword = get_meta_ident(&meta)?; + if SERDE_KEYWORDS.contains(&keyword.as_ref()) && !keyword.ends_with("with") { + Some((meta, keyword)) + } else { + None + } + }) + .unzip(); if schemars_meta_names.contains("skip") { schemars_meta_names.insert("skip_serializing".to_string()); @@ -89,12 +90,8 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { } // Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes - for meta in serde_attrs - .into_iter() - .flat_map(|at| get_meta_items(ctxt, &at)) - .flatten() - { - if let Ok(i) = get_meta_ident(ctxt, &meta) { + for meta in get_meta_items(&serde_attrs, "serde", ctxt) { + if let Some(i) = get_meta_ident(&meta) { if !schemars_meta_names.contains(&i) && SERDE_KEYWORDS.contains(&i.as_ref()) && i != "bound" @@ -125,34 +122,8 @@ fn to_tokens(attrs: &[Attribute]) -> impl ToTokens { tokens } -fn get_meta_items(ctxt: &Ctxt, attr: &Attribute) -> Result, ()> { - match attr.parse_meta() { - Ok(Meta::List(meta)) => Ok(meta.nested.into_iter().collect()), - Ok(_) => { - ctxt.error_spanned_by(attr, "expected #[schemars(...)] or #[serde(...)]"); - Err(()) - } - Err(err) => { - ctxt.error_spanned_by(attr, err); - Err(()) - } - } -} - -fn get_meta_ident(ctxt: &Ctxt, meta: &NestedMeta) -> Result { - match meta { - NestedMeta::Meta(m) => m.path().get_ident().map(|i| i.to_string()).ok_or(()), - NestedMeta::Lit(lit) => { - ctxt.error_spanned_by( - meta, - format!( - "unexpected literal in attribute: {}", - lit.into_token_stream() - ), - ); - Err(()) - } - } +fn get_meta_ident(meta: &Meta) -> Option { + meta.path().get_ident().map(|i| i.to_string()) } #[cfg(test)] @@ -198,7 +169,7 @@ mod tests { }; if let Err(e) = process_serde_attrs(&mut input) { - panic!("process_serde_attrs returned error: {}", e[0]) + panic!("process_serde_attrs returned error: {}", e) }; assert_eq!(input, expected); diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index a089a1de..c8bf8806 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -1,17 +1,23 @@ -use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str}; use proc_macro2::TokenStream; -use serde_derive_internals::Ctxt; -use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta, Path}; +use syn::{Expr, Meta}; -pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[ - "range", "regex", "contains", "email", "phone", "url", "length", "required", -]; +use crate::idents::SCHEMA; + +use super::{ + parse_meta::{ + parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern, + parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange, + }, + AttrCtxt, +}; #[derive(Debug, Clone, Copy, PartialEq)] -enum Format { +pub enum Format { Email, Uri, - Phone, + Ip, + Ipv4, + Ipv6, } impl Format { @@ -19,7 +25,9 @@ impl Format { match self { Format::Email => "email", Format::Uri => "url", - Format::Phone => "phone", + Format::Ip => "ip", + Format::Ipv4 => "ipv4", + Format::Ipv6 => "ipv6", } } @@ -27,470 +35,199 @@ impl Format { match self { Format::Email => "email", Format::Uri => "uri", - Format::Phone => "phone", + Format::Ip => "ip", + Format::Ipv4 => "ipv4", + Format::Ipv6 => "ipv6", } } + + fn from_attr_str(s: &str) -> Option { + Some(match s { + "email" => Format::Email, + "url" => Format::Uri, + "ip" => Format::Ip, + "ipv4" => Format::Ipv4, + "ipv6" => Format::Ipv6, + _ => return None, + }) + } } #[derive(Debug, Default)] pub struct ValidationAttrs { - length_min: Option, - length_max: Option, - length_equal: Option, - range_min: Option, - range_max: Option, - regex: Option, - contains: Option, - required: bool, - format: Option, + pub length: Option, + pub range: Option, + pub pattern: Option, + pub regex: Option, + pub contains: Option, + pub required: bool, + pub format: Option, + pub inner: Option>, } impl ValidationAttrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - ValidationAttrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "validate", true, errors) + pub fn add_mutators(&self, mutators: &mut Vec) { + self.add_mutators2(mutators, "e!(&mut #SCHEMA)); } - pub fn required(&self) -> bool { - self.required - } - - fn populate( - mut self, - attrs: &[syn::Attribute], - attr_type: &'static str, - ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |path: &Path| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - path.get_ident().unwrap() - ); - errors.error_spanned_by(path, msg) - } - }; - let mutual_exclusive_error = |path: &Path, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - path.get_ident().unwrap(), - other, - ); - errors.error_spanned_by(path, msg) - } - }; - let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| { - if !ignore_errors { - let msg = if existing == new { - format!("duplicate schemars attribute `{}`", existing.attr_str()) - } else { - format!( - "schemars attribute cannot contain both `{}` and `{}`", - existing.attr_str(), - new.attr_str(), - ) - }; - errors.error_spanned_by(path, msg) - } - }; - - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { - match &meta_item { - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => { - for nested in meta_list.nested.iter() { - match nested { - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => { - if self.length_min.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_min = str_or_num_to_expr(errors, "min", &nv.lit); - } - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => { - if self.length_max.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_max = str_or_num_to_expr(errors, "max", &nv.lit); - } - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => { - if self.length_equal.is_some() { - duplicate_error(&nv.path) - } else if self.length_min.is_some() { - mutual_exclusive_error(&nv.path, "min") - } else if self.length_max.is_some() { - mutual_exclusive_error(&nv.path, "max") - } else { - self.length_equal = - str_or_num_to_expr(errors, "equal", &nv.lit); - } - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars length attribute".to_string(), - ); - } - } - } - } - } - - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("range") => { - for nested in meta_list.nested.iter() { - match nested { - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => { - if self.range_min.is_some() { - duplicate_error(&nv.path) - } else { - self.range_min = str_or_num_to_expr(errors, "min", &nv.lit); - } - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => { - if self.range_max.is_some() { - duplicate_error(&nv.path) - } else { - self.range_max = str_or_num_to_expr(errors, "max", &nv.lit); - } - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars range attribute".to_string(), - ); - } - } - } - } - } - - NestedMeta::Meta(Meta::Path(m)) - if m.is_ident("required") || m.is_ident("required_nested") => - { - self.required = true; - } + fn add_mutators2(&self, mutators: &mut Vec, mut_ref_schema: &TokenStream) { + if let Some(length) = &self.length { + Self::add_length_or_range(length, mutators, "string", "Length", mut_ref_schema); + Self::add_length_or_range(length, mutators, "array", "Items", mut_ref_schema); + } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Email.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Email, p), - None => self.format = Some(Format::Email), - } - } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Uri.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Uri, p), - None => self.format = Some(Format::Uri), - } - } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Phone.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Phone, p), - None => self.format = Some(Format::Phone), - } - } + if let Some(range) = &self.range { + Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema); + } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&nv.path), - (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"), - (None, None) => { - self.regex = - parse_lit_into_expr_path(errors, attr_type, "regex", &nv.lit).ok() - } - } - } + if let Some(regex) = self.regex.as_ref().or(self.pattern.as_ref()) { + mutators.push(quote! { + schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string()); + }); + } - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"), - (None, None) => { - for x in meta_list.nested.iter() { - match x { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("path") => { - self.regex = - parse_lit_into_expr_path(errors, attr_type, "path", lit) - .ok() - } - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("pattern") => { - self.regex = get_lit_str(errors, attr_type, "pattern", lit) - .ok() - .map(|litstr| { - Expr::Lit(syn::ExprLit { - attrs: Vec::new(), - lit: Lit::Str(litstr.clone()), - }) - }) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars regex attribute".to_string(), - ); - } - } - } - } - } - } - } + if let Some(contains) = &self.contains { + mutators.push(quote! { + schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string()); + }); + } - NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) - if path.is_ident("contains") => - { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(path), - (None, Some(_)) => mutual_exclusive_error(path, "regex"), - (None, None) => { - self.contains = get_lit_str(errors, attr_type, "contains", lit) - .map(|litstr| litstr.value()) - .ok() - } - } - } + if let Some(format) = &self.format { + let f = format.schema_str(); + mutators.push(quote! { + (#mut_ref_schema).ensure_object().insert("format".into(), #f.into()); + }) + }; - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"), - (None, None) => { - for x in meta_list.nested.iter() { - match x { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("pattern") => { - self.contains = - get_lit_str(errors, attr_type, "contains", lit) - .ok() - .map(|litstr| litstr.value()) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars contains attribute".to_string(), - ); - } - } - } - } - } - } - } + if let Some(inner) = &self.inner { + let mut inner_mutators = Vec::new(); + inner.add_mutators2(&mut inner_mutators, "e!(inner_schema)); - _ => {} + if !inner_mutators.is_empty() { + mutators.push(quote! { + schemars::_private::apply_inner_validation(#mut_ref_schema, |inner_schema| { #(#inner_mutators)* }); + }) } } - self } - pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { - 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()) - { - string_validation.push(quote! { - validation.min_length = Some(#length_min as u32); - }); - array_validation.push(quote! { - validation.min_items = Some(#length_min as u32); + fn add_length_or_range( + value: &LengthOrRange, + mutators: &mut Vec, + required_format: &str, + key_suffix: &str, + mut_ref_schema: &TokenStream, + ) { + if let Some(min) = value.min.as_ref().or(value.equal.as_ref()) { + let key = format!("min{key_suffix}"); + mutators.push(quote!{ + schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #min); }); } - 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); - }); - array_validation.push(quote! { - validation.max_items = Some(#length_max as u32); + if let Some(max) = value.max.as_ref().or(value.equal.as_ref()) { + let key = format!("max{key_suffix}"); + mutators.push(quote!{ + schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #max); }); } + } - if let Some(range_min) = &self.range_min { - number_validation.push(quote! { - validation.minimum = Some(#range_min as f64); - }); - } + pub(super) fn populate( + &mut self, + schemars_cx: &mut AttrCtxt, + validate_cx: &mut AttrCtxt, + garde_cx: &mut AttrCtxt, + ) { + self.process_attr(schemars_cx); + self.process_attr(validate_cx); + self.process_attr(garde_cx); + } - if let Some(range_max) = &self.range_max { - number_validation.push(quote! { - validation.maximum = Some(#range_max as f64); - }); - } + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } - if let Some(regex) = &self.regex { - string_validation.push(quote! { - validation.pattern = Some(#regex.to_string()); - }); + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + if let Some(format) = Format::from_attr_str(meta_name) { + self.handle_format(meta, format, cx); + return None; } - - if let Some(contains) = &self.contains { - object_validation.push(quote! { - validation.required.insert(#contains.to_string()); - }); - - if self.regex.is_none() { - let pattern = crate::regex_syntax::escape(contains); - string_validation.push(quote! { - validation.pattern = Some(#pattern.to_string()); - }); + match meta_name { + "length" => match self.length { + Some(_) => cx.duplicate_error(&meta), + None => self.length = parse_length_or_range(meta, cx).ok(), + }, + + "range" => match self.range { + Some(_) => cx.duplicate_error(&meta), + None => self.range = parse_length_or_range(meta, cx).ok(), + }, + + "required" => { + if self.required { + cx.duplicate_error(&meta); + } else if require_path_only(meta, cx).is_ok() { + self.required = true; + } } - } - let format = self.format.as_ref().map(|f| { - let f = f.schema_str(); - quote! { - schema_object.format = Some(#f.to_string()); + "pattern" if cx.attr_type != "validate" => { + match (&self.pattern, &self.regex, &self.contains) { + (Some(_p), _, _) => cx.duplicate_error(&meta), + (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"), + (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"), + (None, None, None) => self.pattern = parse_pattern(meta, cx).ok(), + } } - }); - - let array_validation = wrap_array_validation(array_validation); - let number_validation = wrap_number_validation(number_validation); - let object_validation = wrap_object_validation(object_validation); - let string_validation = wrap_string_validation(string_validation); - - if array_validation.is_some() - || number_validation.is_some() - || object_validation.is_some() - || string_validation.is_some() - || format.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 + "regex" if cx.attr_type != "garde" => { + match (&self.pattern, &self.regex, &self.contains) { + (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"), + (_, Some(_r), _) => cx.duplicate_error(&meta), + (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"), + (None, None, None) => { + if cx.attr_type == "validate" { + self.regex = parse_validate_regex(meta, cx).ok() + } else { + self.regex = parse_schemars_regex(meta, cx).ok() + } } - schema } } - } - } -} - -fn parse_lit_into_expr_path( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - parse_lit_into_path(cx, attr_type, meta_item_name, lit).map(|path| { - Expr::Path(ExprPath { - attrs: Vec::new(), - qself: None, - path, - }) - }) -} - -fn wrap_array_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Array) { - let validation = schema_object.array(); - #(#v)* + "contains" => match (&self.pattern, &self.regex, &self.contains) { + (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"), + (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"), + (_, _, Some(_c)) => cx.duplicate_error(&meta), + (None, None, None) => self.contains = parse_contains(meta, cx).ok(), + }, + + "inner" if cx.attr_type != "validate" => { + if let Ok(nested_meta) = parse_nested_meta(meta, cx) { + let inner = self + .inner + .get_or_insert_with(|| Box::new(ValidationAttrs::default())); + let mut inner_cx = cx.new_nested_meta(nested_meta.into_iter().collect()); + inner.process_attr(&mut inner_cx); + } } - }) - } -} -fn wrap_number_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Integer) - || schema_object.has_type(schemars::schema::InstanceType::Number) { - let validation = schema_object.number(); - #(#v)* - } - }) - } -} + _ => return Some(meta), + } -fn wrap_object_validation(v: Vec) -> Option { - if v.is_empty() { None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Object) { - let validation = schema_object.object(); - #(#v)* - } - }) } -} -fn wrap_string_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::String) { - let validation = schema_object.string(); - #(#v)* + fn handle_format(&mut self, meta: Meta, format: Format, cx: &AttrCtxt) { + match self.format { + Some(current) if current == format => cx.duplicate_error(&meta), + Some(current) => cx.mutual_exclusive_error(&meta, current.attr_str()), + None => { + // Allow a MetaList in validator attr (e.g. with message/code items), + // but restrict it to path only in schemars attr. + if cx.attr_type == "validate" || require_path_only(meta, cx).is_ok() { + self.format = Some(format); + } } - }) - } -} - -fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option { - match lit { - Lit::Str(s) => parse_lit_str::(s).ok().map(Expr::Path), - Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit { - attrs: Vec::new(), - lit: lit.clone(), - })), - _ => { - cx.error_spanned_by( - lit, - format!( - "expected `{}` to be a string or number literal", - meta_item_name - ), - ); - None } } } diff --git a/schemars_derive/src/idents.rs b/schemars_derive/src/idents.rs new file mode 100644 index 00000000..d940a626 --- /dev/null +++ b/schemars_derive/src/idents.rs @@ -0,0 +1,15 @@ +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; +use quote::TokenStreamExt; + +pub const GENERATOR: ConstIdent = ConstIdent("generator"); +pub const SCHEMA: ConstIdent = ConstIdent("schema"); +pub const STRUCT_DEFAULT: ConstIdent = ConstIdent("struct_default"); + +pub struct ConstIdent(&'static str); + +impl quote::ToTokens for ConstIdent { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = Ident::new(self.0, Span::call_site()); + tokens.append(TokenTree::from(ident)); + } +} diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index f8173f42..7b810ec4 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -8,19 +8,21 @@ extern crate proc_macro; mod ast; mod attr; -mod metadata; -mod regex_syntax; +mod idents; mod schema_exprs; use ast::*; +use idents::GENERATOR; use proc_macro2::TokenStream; use syn::spanned::Spanned; -#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate))] +#[doc = "Derive macro for `JsonSchema` trait."] +#[cfg_attr(not(doctest), doc = include_str!("../deriving.md"), doc = include_str!("../attributes.md"))] +#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate, garde))] pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as syn::DeriveInput); derive_json_schema(input, false) - .unwrap_or_else(compile_error) + .unwrap_or_else(syn::Error::into_compile_error) .into() } @@ -28,14 +30,11 @@ pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro: pub fn derive_json_schema_repr_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as syn::DeriveInput); derive_json_schema(input, true) - .unwrap_or_else(compile_error) + .unwrap_or_else(syn::Error::into_compile_error) .into() } -fn derive_json_schema( - mut input: syn::DeriveInput, - repr: bool, -) -> Result> { +fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result { attr::process_serde_attrs(&mut input)?; let mut cont = Container::from_ast(&input)?; @@ -59,20 +58,24 @@ fn derive_json_schema( #[automatically_derived] impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause { - fn is_referenceable() -> bool { - <#ty as schemars::JsonSchema>::is_referenceable() + fn always_inline_schema() -> bool { + <#ty as schemars::JsonSchema>::always_inline_schema() } - fn schema_name() -> std::string::String { + fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { <#ty as schemars::JsonSchema>::schema_name() } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - <#ty as schemars::JsonSchema>::json_schema(gen) + fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { + <#ty as schemars::JsonSchema>::schema_id() } - fn _schemars_private_non_optional_json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(gen) + fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) + } + + fn _schemars_private_non_optional_json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } fn _schemars_private_is_option() -> bool { @@ -83,7 +86,7 @@ fn derive_json_schema( }); } - let mut schema_base_name = cont.name(); + let mut schema_base_name = cont.name().to_string(); if !cont.attrs.is_renamed { if let Some(path) = cont.serde_attrs.remote() { @@ -95,30 +98,77 @@ 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)); - } + schemars::_private::alloc::borrow::Cow::Borrowed(#schema_base_name) + }, 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)); + schemars::_private::alloc::borrow::Cow::Borrowed(::core::concat!( + ::core::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())*) - } - }; + schemars::_private::alloc::borrow::Cow::Owned( + schemars::_private::alloc::format!(#schema_name_fmt #(,#type_params=#type_params::schema_name())* #(,#const_params=#const_params)*) + ) + }, + quote! { + schemars::_private::alloc::borrow::Cow::Owned( + schemars::_private::alloc::format!( + ::core::concat!( + ::core::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! { + schemars::_private::alloc::borrow::Cow::Owned( + schemars::_private::alloc::format!(#schema_name_fmt #(,#type_params::schema_name())* #(,#const_params)*) + ) + }, + quote! { + schemars::_private::alloc::borrow::Cow::Owned( + schemars::_private::alloc::format!( + ::core::concat!( + ::core::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])? + schema_exprs::expr_for_repr(&cont)? } else { schema_exprs::expr_for_container(&cont) }; @@ -130,11 +180,15 @@ fn derive_json_schema( #[automatically_derived] #[allow(unused_braces)] impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause { - fn schema_name() -> std::string::String { + fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { #schema_name } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { + #schema_id + } + + fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { #schema_expr } }; @@ -147,8 +201,11 @@ fn add_trait_bounds(cont: &mut Container) { let where_clause = cont.generics.make_where_clause(); where_clause.predicates.extend(bounds.iter().cloned()); } else { - // No explicit trait bounds specified, assume the Rust convention of adding the trait to each type parameter - // TODO consider also adding trait bound to associated types when used as fields - I think Serde does this? + // No explicit trait bounds specified, assume the Rust convention of adding the trait to + // each type parameter + // + // TODO consider also adding trait bound to associated types + // when used as fields - I think Serde does this? for param in &mut cont.generics.params { if let syn::GenericParam::Type(ref mut type_param) = *param { type_param.bounds.push(parse_quote!(schemars::JsonSchema)); @@ -156,10 +213,3 @@ fn add_trait_bounds(cont: &mut Container) { } } } - -fn compile_error(errors: Vec) -> TokenStream { - let compile_errors = errors.iter().map(syn::Error::to_compile_error); - quote! { - #(#compile_errors)* - } -} diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs deleted file mode 100644 index 32dbf671..00000000 --- a/schemars_derive/src/metadata.rs +++ /dev/null @@ -1,77 +0,0 @@ -use proc_macro2::TokenStream; - -#[derive(Debug, Clone)] -pub struct SchemaMetadata<'a> { - pub title: Option<&'a str>, - pub description: Option<&'a str>, - pub deprecated: bool, - pub read_only: bool, - pub write_only: bool, - pub examples: &'a [syn::Path], - pub default: Option, -} - -impl<'a> SchemaMetadata<'a> { - pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { - let setters = self.make_setters(); - if !setters.is_empty() { - *schema_expr = quote! { - schemars::_private::apply_metadata(#schema_expr, schemars::schema::Metadata { - #(#setters)* - ..Default::default() - }) - } - } - } - - fn make_setters(&self) -> Vec { - let mut setters = Vec::::new(); - - if let Some(title) = &self.title { - setters.push(quote! { - title: Some(#title.to_owned()), - }); - } - if let Some(description) = &self.description { - setters.push(quote! { - description: Some(#description.to_owned()), - }); - } - - if self.deprecated { - setters.push(quote! { - deprecated: true, - }); - } - - if self.read_only { - setters.push(quote! { - read_only: true, - }); - } - if self.write_only { - setters.push(quote! { - write_only: true, - }); - } - - if !self.examples.is_empty() { - let examples = self.examples.iter().map(|eg| { - quote! { - schemars::_serde_json::value::to_value(#eg()) - } - }); - setters.push(quote! { - examples: vec![#(#examples),*].into_iter().flatten().collect(), - }); - } - - if let Some(default) = &self.default { - setters.push(quote! { - default: #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)), - }); - } - - setters - } -} diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 1f55084b..3e1b4862 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -1,12 +1,55 @@ -use std::collections::HashSet; - -use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata}; +use crate::{ast::*, attr::WithAttr, idents::*}; use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; use serde_derive_internals::ast::Style; use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType}; +use std::collections::HashSet; use syn::spanned::Spanned; -pub fn expr_for_container(cont: &Container) -> TokenStream { +pub struct SchemaExpr { + /// Definitions for types or functions that may be used within the creator or mutators + definitions: Vec, + /// An expression that produces a `Schema` + creator: TokenStream, + /// Statements (including terminating semicolon) that mutate a var `schema` of type `Schema` + mutators: Vec, +} + +impl From for SchemaExpr { + fn from(creator: TokenStream) -> Self { + Self { + definitions: Vec::new(), + creator, + mutators: Vec::new(), + } + } +} + +impl ToTokens for SchemaExpr { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { + definitions, + creator, + mutators, + } = self; + + tokens.extend(if mutators.is_empty() { + quote!({ + #(#definitions)* + #creator + }) + } else { + quote!({ + #(#definitions)* + let mut #SCHEMA = #creator; + #(#mutators)* + #SCHEMA + }) + }); + } +} + +pub fn expr_for_container(cont: &Container) -> SchemaExpr { let mut schema_expr = match &cont.data { Data::Struct(Style::Unit, _) => expr_for_unit_struct(), Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]), @@ -19,11 +62,11 @@ pub fn expr_for_container(cont: &Container) -> TokenStream { Data::Enum(variants) => expr_for_enum(variants, &cont.serde_attrs), }; - cont.attrs.as_metadata().apply_to_schema(&mut schema_expr); + cont.add_mutators(&mut schema_expr.mutators); schema_expr } -pub fn expr_for_repr(cont: &Container) -> Result { +pub fn expr_for_repr(cont: &Container) -> Result { let repr_type = cont.attrs.repr.as_ref().ok_or_else(|| { syn::Error::new( Span::call_site(), @@ -33,13 +76,18 @@ pub fn expr_for_repr(cont: &Container) -> Result { let variants = match &cont.data { Data::Enum(variants) => variants, - _ => return Err(syn::Error::new(Span::call_site(), "oh no!")), + _ => { + return Err(syn::Error::new( + Span::call_site(), + "JsonSchema_repr can only be used on enums", + )) + } }; if let Some(non_unit_error) = variants.iter().find_map(|v| match v.style { Style::Unit => None, - _ => Some(syn::Error::new( - v.original.span(), + _ => Some(syn::Error::new_spanned( + v.original, "JsonSchema_repr: must be a unit variant", )), }) { @@ -49,36 +97,45 @@ pub fn expr_for_repr(cont: &Container) -> Result { let enum_ident = &cont.ident; let variant_idents = variants.iter().map(|v| &v.ident); - let mut schema_expr = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Integer.into()), - enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]), - }); + let mut schema_expr = SchemaExpr::from(quote!({ + let mut map = schemars::_private::serde_json::Map::new(); + map.insert("type".into(), "integer".into()); + map.insert( + "enum".into(), + schemars::_private::serde_json::Value::Array({ + let mut enum_values = schemars::_private::alloc::vec::Vec::new(); + #(enum_values.push((#enum_ident::#variant_idents as #repr_type).into());)* + enum_values + }), + ); + schemars::Schema::from(map) + })); + + cont.add_mutators(&mut schema_expr.mutators); - cont.attrs.as_metadata().apply_to_schema(&mut schema_expr); Ok(schema_expr) } -fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream { +fn expr_for_field(field: &Field, allow_ref: bool) -> SchemaExpr { let (ty, type_def) = type_for_field_schema(field); let span = field.original.span(); - let gen = quote!(gen); - let mut schema_expr = if field.validation_attrs.required() { + let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required { quote_spanned! {span=> - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen) + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } } else if allow_ref { quote_spanned! {span=> - #gen.subschema_for::<#ty>() + #GENERATOR.subschema_for::<#ty>() } } else { quote_spanned! {span=> - <#ty as schemars::JsonSchema>::json_schema(#gen) + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) } - }; + }); - prepend_type_def(type_def, &mut schema_expr); - field.validation_attrs.apply_to_schema(&mut schema_expr); + schema_expr.definitions.extend(type_def); + field.add_mutators(&mut schema_expr.mutators); schema_expr } @@ -101,16 +158,25 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option) { struct #ty_name; impl schemars::JsonSchema for #ty_name { - fn is_referenceable() -> bool { - false + fn always_inline_schema() -> bool { + true + } + + fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { + schemars::_private::alloc::borrow::Cow::Borrowed(#fn_name) } - fn schema_name() -> std::string::String { - #fn_name.to_string() + fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { + schemars::_private::alloc::borrow::Cow::Borrowed(::core::concat!( + "_SchemarsSchemaWithFunction/", + ::core::module_path!(), + "/", + #fn_name + )) } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - #fun(gen) + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + #fun(generator) } } }; @@ -120,7 +186,7 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option) { } } -fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream { +fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> SchemaExpr { let deny_unknown_fields = cattrs.deny_unknown_fields(); let variants = variants .iter() @@ -141,8 +207,8 @@ fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenS fn expr_for_external_tagged_enum<'a>( variants: impl Iterator>, deny_unknown_fields: bool, -) -> TokenStream { - let mut unique_names = HashSet::::new(); +) -> SchemaExpr { + let mut unique_names = HashSet::<&str>::new(); let mut count = 0; let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = variants .inspect(|v| { @@ -151,10 +217,19 @@ fn expr_for_external_tagged_enum<'a>( }) .partition(|v| v.is_unit() && v.attrs.is_default()); let unit_names = unit_variants.iter().map(|v| v.name()); - let unit_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#(#unit_names.into()),*]), - }); + let unit_schema = SchemaExpr::from(quote!({ + let mut map = schemars::_private::serde_json::Map::new(); + map.insert("type".into(), "string".into()); + map.insert( + "enum".into(), + schemars::_private::serde_json::Value::Array({ + let mut enum_values = schemars::_private::alloc::vec::Vec::new(); + #(enum_values.push((#unit_names).into());)* + enum_values + }), + ); + schemars::Schema::from(map) + })); if complex_variants.is_empty() { return unit_schema; @@ -168,42 +243,19 @@ fn expr_for_external_tagged_enum<'a>( schemas.extend(complex_variants.into_iter().map(|variant| { let name = variant.name(); - let mut schema_expr = if variant.is_unit() && variant.attrs.with.is_none() { - schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }) - } else { - let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields); - schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - object: Some(Box::new(schemars::schema::ObjectValidation { - properties: { - let mut props = schemars::Map::new(); - props.insert(#name.to_owned(), #sub_schema); - props - }, - required: { - let mut required = schemars::Set::new(); - required.insert(#name.to_owned()); - required - }, - // Externally tagged variants must prohibit additional - // properties irrespective of the disposition of - // `deny_unknown_fields`. If additional properties were allowed - // one could easily construct an object that validated against - // multiple variants since here it's the properties rather than - // the values of a property that distingish between variants. - additional_properties: Some(Box::new(false.into())), - ..Default::default() - })), - }) - }; + let mut schema_expr = + SchemaExpr::from(if variant.is_unit() && variant.attrs.with.is_none() { + quote! { + schemars::_private::new_unit_enum_variant(#name) + } + } else { + let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields); + quote! { + schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema) + } + }); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut schema_expr); + variant.add_mutators(&mut schema_expr.mutators); schema_expr })); @@ -215,56 +267,24 @@ fn expr_for_internal_tagged_enum<'a>( variants: impl Iterator>, tag_name: &str, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let mut unique_names = HashSet::new(); let mut count = 0; - let set_additional_properties = if deny_unknown_fields { - quote! { - additional_properties: Some(Box::new(false.into())), - } - } else { - TokenStream::new() - }; let variant_schemas = variants .map(|variant| { unique_names.insert(variant.name()); count += 1; - let name = variant.name(); - let type_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }); - - let mut tag_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - object: Some(Box::new(schemars::schema::ObjectValidation { - properties: { - let mut props = schemars::Map::new(); - props.insert(#tag_name.to_owned(), #type_schema); - props - }, - required: { - let mut required = schemars::Set::new(); - required.insert(#tag_name.to_owned()); - required - }, - // As we're creating a "wrapper" object, we can honor the - // disposition of deny_unknown_fields. - #set_additional_properties - ..Default::default() - })), - }); + let mut schema_expr = expr_for_internal_tagged_enum_variant(variant, deny_unknown_fields); - variant.attrs.as_metadata().apply_to_schema(&mut tag_schema); + let name = variant.name(); + schema_expr.mutators.push(quote!( + schemars::_private::apply_internal_enum_variant_tag(&mut #SCHEMA, #tag_name, #name, #deny_unknown_fields); + )); - if let Some(variant_schema) = - expr_for_untagged_enum_variant_for_flatten(variant, deny_unknown_fields) - { - tag_schema.extend(quote!(.flatten(#variant_schema))) - } + variant.add_mutators(&mut schema_expr.mutators); - tag_schema + schema_expr }) .collect(); @@ -274,15 +294,12 @@ fn expr_for_internal_tagged_enum<'a>( fn expr_for_untagged_enum<'a>( variants: impl Iterator>, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let schemas = variants .map(|variant| { let mut schema_expr = expr_for_untagged_enum_variant(variant, deny_unknown_fields); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut schema_expr); + variant.add_mutators(&mut schema_expr.mutators); schema_expr }) @@ -298,7 +315,7 @@ fn expr_for_adjacent_tagged_enum<'a>( tag_name: &str, content_name: &str, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let mut unique_names = HashSet::new(); let mut count = 0; let schemas = variants @@ -315,52 +332,44 @@ fn expr_for_adjacent_tagged_enum<'a>( let (add_content_to_props, add_content_to_required) = content_schema .map(|content_schema| { ( - quote!(props.insert(#content_name.to_owned(), #content_schema);), - quote!(required.insert(#content_name.to_owned());), + quote!(#content_name: (#content_schema),), + quote!(#content_name,), ) }) .unwrap_or_default(); let name = variant.name(); - let tag_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }); + let tag_schema = quote! { + schemars::json_schema!({ + "type": "string", + "enum": [#name], + }) + }; let set_additional_properties = if deny_unknown_fields { quote! { - additional_properties: Some(Box::new(false.into())), + "additionalProperties": false, } } else { TokenStream::new() }; - let mut outer_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - object: Some(Box::new(schemars::schema::ObjectValidation { - properties: { - let mut props = schemars::Map::new(); - props.insert(#tag_name.to_owned(), #tag_schema); - #add_content_to_props - props - }, - required: { - let mut required = schemars::Set::new(); - required.insert(#tag_name.to_owned()); - #add_content_to_required - required - }, - // As we're creating a "wrapper" object, we can honor the - // disposition of deny_unknown_fields. - #set_additional_properties - ..Default::default() - })), - }); + let mut outer_schema = SchemaExpr::from(quote!(schemars::json_schema!({ + "type": "object", + "properties": { + #tag_name: (#tag_schema), + #add_content_to_props + }, + "required": [ + #tag_name, + #add_content_to_required + ], + // As we're creating a "wrapper" object, we can honor the + // disposition of deny_unknown_fields. + #set_additional_properties + }))); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut outer_schema); + variant.add_mutators(&mut outer_schema.mutators); outer_schema }) @@ -371,33 +380,32 @@ fn expr_for_adjacent_tagged_enum<'a>( /// Callers must determine if all subschemas are mutually exclusive. This can /// be done for most tagging regimes by checking that all tag names are unique. -fn variant_subschemas(unique: bool, schemas: Vec) -> TokenStream { - if unique { - schema_object(quote! { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - one_of: Some(vec![#(#schemas),*]), - ..Default::default() - })), - }) - } else { - schema_object(quote! { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(vec![#(#schemas),*]), - ..Default::default() - })), - }) - } +fn variant_subschemas(unique: bool, schemas: Vec) -> SchemaExpr { + let keyword = if unique { "oneOf" } else { "anyOf" }; + quote!({ + let mut map = schemars::_private::serde_json::Map::new(); + map.insert( + #keyword.into(), + schemars::_private::serde_json::Value::Array({ + let mut enum_values = schemars::_private::alloc::vec::Vec::new(); + #(enum_values.push(#schemas.to_value());)* + enum_values + }), + ); + schemars::Schema::from(map) + }) + .into() } -fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream { +fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> SchemaExpr { if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(with_attr); - let gen = quote!(gen); - let mut schema_expr = quote_spanned! {variant.original.span()=> - #gen.subschema_for::<#ty>() - }; + let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> + #GENERATOR.subschema_for::<#ty>() + }); + + schema_expr.definitions.extend(type_def); - prepend_type_def(type_def, &mut schema_expr); return schema_expr; } @@ -409,40 +417,41 @@ fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) } } -fn expr_for_untagged_enum_variant_for_flatten( +fn expr_for_internal_tagged_enum_variant( variant: &Variant, deny_unknown_fields: bool, -) -> Option { +) -> SchemaExpr { if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(with_attr); - let gen = quote!(gen); - let mut schema_expr = quote_spanned! {variant.original.span()=> - <#ty as schemars::JsonSchema>::json_schema(#gen) - }; + let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) + }); - prepend_type_def(type_def, &mut schema_expr); - return Some(schema_expr); + schema_expr.definitions.extend(type_def); + + return schema_expr; } - Some(match variant.style { - Style::Unit => return None, + match variant.style { + Style::Unit => expr_for_unit_struct(), Style::Newtype => expr_for_field(&variant.fields[0], false), Style::Tuple => expr_for_tuple_struct(&variant.fields), Style::Struct => expr_for_struct(&variant.fields, &SerdeDefault::None, deny_unknown_fields), - }) + } } -fn expr_for_unit_struct() -> TokenStream { +fn expr_for_unit_struct() -> SchemaExpr { quote! { - gen.subschema_for::<()>() + #GENERATOR.subschema_for::<()>() } + .into() } -fn expr_for_newtype_struct(field: &Field) -> TokenStream { +fn expr_for_newtype_struct(field: &Field) -> SchemaExpr { expr_for_field(field, true) } -fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { +fn expr_for_tuple_struct(fields: &[Field]) -> SchemaExpr { let fields: Vec<_> = fields .iter() .filter(|f| !f.serde_attrs.skip_deserializing()) @@ -451,126 +460,95 @@ fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { let len = fields.len() as u32; quote! { - schemars::schema::Schema::Object( - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::Array.into()), - array: Some(Box::new(schemars::schema::ArrayValidation { - items: Some(vec![#(#fields),*].into()), - max_items: Some(#len), - min_items: Some(#len), - ..Default::default() - })), - ..Default::default() + schemars::json_schema!({ + "type": "array", + "prefixItems": [#((#fields)),*], + "minItems": #len, + "maxItems": #len, }) } + .into() } fn expr_for_struct( fields: &[Field], default: &SerdeDefault, deny_unknown_fields: bool, -) -> TokenStream { - let (flattened_fields, property_fields): (Vec<_>, Vec<_>) = fields - .iter() - .filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing()) - .partition(|f| f.serde_attrs.flatten()); - +) -> SchemaExpr { let set_container_default = match default { SerdeDefault::None => None, - SerdeDefault::Default => Some(quote!(let container_default = Self::default();)), - SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)), + SerdeDefault::Default => Some(quote!(let #STRUCT_DEFAULT = Self::default();)), + SerdeDefault::Path(path) => Some(quote!(let #STRUCT_DEFAULT = #path();)), }; - let properties: Vec<_> = property_fields - .into_iter() + // a vec of mutators + let properties: Vec = fields + .iter() + .filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing()) .map(|field| { - let name = field.name(); - let default = field_default_expr(field, set_container_default.is_some()); - - let (ty, type_def) = type_for_field_schema(field); - - let maybe_insert_required = match (&default, field.validation_attrs.required()) { - (Some(_), _) => TokenStream::new(), - (None, false) => { - quote! { - if !<#ty as schemars::JsonSchema>::_schemars_private_is_option() { - object_validation.required.insert(#name.to_owned()); - } - } - } - (None, true) => quote! { - object_validation.required.insert(#name.to_owned()); - }, - }; + if field.serde_attrs.flatten() { + let (ty, type_def) = type_for_field_schema(field); - let metadata = SchemaMetadata { - read_only: field.serde_attrs.skip_deserializing(), - write_only: field.serde_attrs.skip_serializing(), - default, - ..field.attrs.as_metadata() - }; + let required = field.attrs.validation.required; + let mut schema_expr = SchemaExpr::from(quote_spanned! {ty.span()=> + schemars::_private::json_schema_for_flatten::<#ty>(#GENERATOR, #required) + }); - let gen = quote!(gen); - let mut schema_expr = if field.validation_attrs.required() { - quote_spanned! {ty.span()=> - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen) + schema_expr.definitions.extend(type_def); + + quote! { + schemars::_private::flatten(&mut #SCHEMA, #schema_expr); } } else { - quote_spanned! {ty.span()=> - #gen.subschema_for::<#ty>() - } - }; + let name = field.name(); + let (ty, type_def) = type_for_field_schema(field); - metadata.apply_to_schema(&mut schema_expr); - field.validation_attrs.apply_to_schema(&mut schema_expr); + let has_default = set_container_default.is_some() || !field.serde_attrs.default().is_none(); + let required = field.attrs.validation.required; - quote! { - { - #type_def - object_validation.properties.insert(#name.to_owned(), #schema_expr); - #maybe_insert_required + let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required { + quote_spanned! {ty.span()=> + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) + } + } else { + quote_spanned! {ty.span()=> + #GENERATOR.subschema_for::<#ty>() + } + }); + + field.add_mutators(&mut schema_expr.mutators); + if let Some(default) = field_default_expr(field, set_container_default.is_some()) { + schema_expr.mutators.push(quote! { + #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) + .map(|d| schemars::_private::insert_metadata_property(&mut #SCHEMA, "default", d)); + }) } - } - }) - .collect(); - - let flattens: Vec<_> = flattened_fields - .into_iter() - .map(|field| { - let (ty, type_def) = type_for_field_schema(field); - - let required = field.validation_attrs.required(); - - let args = quote!(gen, #required); - let mut schema_expr = quote_spanned! {ty.span()=> - schemars::_private::json_schema_for_flatten::<#ty>(#args) - }; - prepend_type_def(type_def, &mut schema_expr); - schema_expr - }) + // embed `#type_def` outside of `#schema_expr`, because it's used as the type param + // (i.e. `#type_def` is the definition of `#ty`) + quote!({ + #type_def + schemars::_private::insert_object_property::<#ty>(&mut #SCHEMA, #name, #has_default, #required, #schema_expr); + }) + } + }) .collect(); let set_additional_properties = if deny_unknown_fields { quote! { - object_validation.additional_properties = Some(Box::new(false.into())); + "additionalProperties": false, } } else { TokenStream::new() }; - quote! { - { - #set_container_default - let mut schema_object = schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - ..Default::default() - }; - let object_validation = schema_object.object(); + + SchemaExpr { + definitions: set_container_default.into_iter().collect(), + creator: quote!(schemars::json_schema!({ + "type": "object", #set_additional_properties - #(#properties)* - schemars::schema::Schema::Object(schema_object) - #(.flatten(#flattens))* - } + })), + mutators: properties, } } @@ -584,7 +562,7 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option { let member = &field.member; - quote!(container_default.#member) + quote!(#STRUCT_DEFAULT.#member) } SerdeDefault::Default => quote!(<#ty>::default()), SerdeDefault::Path(path) => quote!(#path()), @@ -613,7 +591,7 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> ::core::result::Result where S: serde::Serializer { @@ -628,24 +606,3 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option TokenStream { - quote! { - schemars::schema::Schema::Object( - schemars::schema::SchemaObject { - #properties - ..Default::default() - }) - } -} - -fn prepend_type_def(type_def: Option, schema_expr: &mut TokenStream) { - if let Some(type_def) = type_def { - *schema_expr = quote! { - { - #type_def - #schema_expr - } - } - } -}