schemars_derive-1.2.1/.cargo_vcs_info.json0000644000000001550000000000100142010ustar { "git": { "sha1": "5ef5da1e9aecd0e949b591fbb1832fe53fa3e8ba" }, "path_in_vcs": "schemars_derive" }schemars_derive-1.2.1/.gitignore000064400000000000000000000000361046102023000147570ustar 00000000000000/target **/*.rs.bk Cargo.lock schemars_derive-1.2.1/Cargo.lock0000644000000037760000000000100121700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "schemars_derive" version = "1.2.1" dependencies = [ "pretty_assertions", "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "serde_derive_internals" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" schemars_derive-1.2.1/Cargo.toml0000644000000025700000000000100122020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.74" name = "schemars_derive" version = "1.2.1" authors = ["Graham Esau "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Macros for #[derive(JsonSchema)], for use with schemars" homepage = "https://graham.cool/schemars/" readme = "README.md" keywords = [ "rust", "json-schema", "serde", ] categories = [ "encoding", "no-std", ] license = "MIT" repository = "https://github.com/GREsau/schemars" [lib] name = "schemars_derive" path = "src/lib.rs" proc-macro = true [dependencies.proc-macro2] version = "1.0.74" [dependencies.quote] version = "1.0.35" [dependencies.serde_derive_internals] version = "0.29.1" [dependencies.syn] version = "2.0.46" [dev-dependencies.pretty_assertions] version = "1.2.1" [dev-dependencies.syn] version = "2.0" features = ["extra-traits"] schemars_derive-1.2.1/Cargo.toml.orig000064400000000000000000000012171046102023000156600ustar 00000000000000[package] name = "schemars_derive" description = "Macros for #[derive(JsonSchema)], for use with schemars" homepage = "https://graham.cool/schemars/" repository = "https://github.com/GREsau/schemars" version = "1.2.1" authors = ["Graham Esau "] edition = "2021" license = "MIT" readme = "README.md" keywords = ["rust", "json-schema", "serde"] categories = ["encoding", "no-std"] rust-version = "1.74" [lib] proc-macro = true [dependencies] proc-macro2 = "1.0.74" quote = "1.0.35" syn = "2.0.46" serde_derive_internals = "0.29.1" [dev-dependencies] syn = { version = "2.0", features = ["extra-traits"] } pretty_assertions = "1.2.1" schemars_derive-1.2.1/LICENSE000064400000000000000000000020541046102023000137760ustar 00000000000000MIT License Copyright (c) 2019 Graham Esau Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. schemars_derive-1.2.1/README.md000064400000000000000000000235301046102023000142520ustar 00000000000000# Schemars [![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) [![API Docs](https://img.shields.io/docsrs/schemars/latest?label=API%20docs)](https://docs.rs/schemars/latest) [![Usage Docs](https://img.shields.io/badge/Usage%20docs-graham.cool%2Fschemars-blue)](https://graham.cool/schemars) [![MSRV 1.74+](https://img.shields.io/badge/msrv-1.74-blue)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0/) Generate JSON Schema documents from Rust code ## Basic Usage _For more detailed information, see the full [API documentation on docs.rs](https://docs.rs/schemars/latest), and the [detailed usage documentation website](https://graham.cool/schemars)._ 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": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", "properties": { "my_bool": { "type": "boolean" }, "my_int": { "type": "integer", "format": "int32" }, "my_nullable_enum": { "anyOf": [ { "$ref": "#/$defs/MyEnum" }, { "type": "null" } ] } }, "required": ["my_int", "my_bool"], "$defs": { "MyEnum": { "oneOf": [ { "type": "object", "properties": { "StringNewType": { "type": "string" } }, "additionalProperties": false, "required": ["StringNewType"] }, { "type": "object", "properties": { "StructVariant": { "type": "object", "properties": { "floats": { "type": "array", "items": { "type": "number", "format": "float" } } }, "required": ["floats"] } }, "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. ```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": "https://json-schema.org/draft/2020-12/schema", "title": "MyStruct", "type": "object", "properties": { "myBool": { "type": "boolean" }, "myNullableEnum": { "anyOf": [ { "$ref": "#/$defs/MyEnum" }, { "type": "null" } ], "default": null }, "myNumber": { "type": "integer", "format": "int32" } }, "additionalProperties": false, "required": ["myNumber", "myBool"], "$defs": { "MyEnum": { "anyOf": [ { "type": "string" }, { "type": "object", "properties": { "floats": { "type": "array", "items": { "type": "number", "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. ### 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 } } ```
## Versioning and Stability Schemars follows semantic versioning, with the following caveats: - Increasing MSRV (Minimum Supported Rust Version) is considered a semver-minor change. Schemars aims to support the past year of stable rust versions, but this is not guaranteed. - External libraries that are supported via optional dependencies (see [Feature Flags](#feature-flags)) _may_ be removed in a minor version change, particularly if a newer semver-incompatible version has been released for a long time. - The exact structure of generated schemas (both for built-in implementations on standard library types, and for `#[derive(JsonSchema)]` implementations) may change between versions of schemars - this is not considered a breaking change. - Exported items that are marked with `#[doc(hidden)]` and have names beginning with `_` are not part of the public API, and may be changed or removed without notice. - If a bug is found in schemars that causes attributes to be incorrectly processed or silently ignored by `#[derive(JsonSchema)]`, a subsequent version of schemars may instead fail compilation when encountering such attributes. This is considered a bug fix, and not a breaking change. ## 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 - `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): - `arrayvec07` - [arrayvec](https://crates.io/crates/arrayvec) (^0.7) - `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) - `indexmap2` - [indexmap](https://crates.io/crates/indexmap) (^2.0) - `jiff02` - [jiff](https://crates.io/crates/jiff) (^0.2) - `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) - `smol_str03` - [smol_str](https://crates.io/crates/smol_str) (^0.3) - `url2` - [url](https://crates.io/crates/url) (^2.0) - `uuid1` - [uuid](https://crates.io/crates/uuid) (^1.0) Bear in mind that each of these feature flags _may_ be removed in a future semver-minor change of Schemars, particularly if a newer semver-incompatible version of the external library has been released for a long time. This is unfortunately necessary to avoid supporting old/unmaintained libraries indefinitely. 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 = "1.0", features = ["chrono04"] } ``` schemars_derive-1.2.1/attributes.md000064400000000000000000000424101046102023000155010ustar 00000000000000# 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. You can also "unset" serde attributes by including them with a `!` prefix in a schemars attribute, which will make schemars ignore the corresponding serde attribute item: ```rust #[derive(Deserialize, Serialize, JsonSchema)] #[serde(from = "OtherType")] // this makes schemars ignore the `from = "OtherType"` from the serde attribute: #[schemars(!from)] pub struct MyStruct { // ... } ``` [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) - [`inline`](#inline) - [`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(untagged)]`/`#[schemars(untagged)]` can also be set on an individual variant of a tagged enum to treat just that variant as untagged. Serde docs: [`tag`](https://serde.rs/container-attrs.html#tag) / [`tag`+`content`](https://serde.rs/container-attrs.html#tag--content) / [`untagged` (enum)](https://serde.rs/container-attrs.html#untagged) / [`untagged` (variant)](https://serde.rs/variant-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 container, 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`. Serde does not allow this attribute to be set on containers, but this is allowed in `#[schemars(...)]` attributes. 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: [from](https://serde.rs/container-attrs.html#from) / [try_from](https://serde.rs/container-attrs.html#try_from)

`#[serde(from = "Type")]` / `#[schemars(from = "Type")]`
`#[serde(try_from = "Type")]` / `#[schemars(try_from = "Type")]`

Set on a container to generate its [**deserialize** schema](https://graham.cool/schemars/generating/#serialize-vs-deserialize-contract) as the given type instead of its actual type. Schemars treats the `from`/`try_from` attributes identically. Serde docs: [into](https://serde.rs/container-attrs.html#into)

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

Set on a container to generate its [**serialize** schema](https://graham.cool/schemars/generating/#serialize-vs-deserialize-contract) as the given type instead of its actual type. Schemars treats the `from`/`try_from` attributes identically.

`#[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 = value)]`

Set on a container, variant or field to include the given value in the generated schema's `examples`. The value can be any type that implements serde's `Serialize` trait - it does not need to be the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. In previous versions of schemars, the value had to be a string literal identifying a defined function that would be called to return the actual example value (similar to the [`default`](#default) attribute). To avoid the new attribute behaviour from silently breaking old consumers, string literals consisting of a single word (e.g. `#[schemars(example = "my_fn")]`) or a path (e.g. `#[schemars(example = "my_mod::my_fn")]`) are currently disallowed. This restriction may be relaxed in a future version of schemars, but for now if you want to include such a string as the literal example value, this can be done by borrowing the value, e.g. `#[schemars(example = &"my_fn")]`. If you instead want to call a function to get the example value (mirrorring the old behaviour), you must use an explicit function call expression, e.g. `#[schemars(example = my_fn())]`. Alternatively, to directly set multiple examples without repeating `example = ...` attribute, you can instead use the [`extend`](#extend) attribute, e.g. `#[schemars(extend("examples" = [1, 2, 3]))]`.

`#[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(inline)]`

Set the return value of [`inline_schema`](trait.JsonSchema.html#method.inline_schema) to `true` to include JSON schemas generated for this type directly in parent schemas, rather than being re-used where possible using the `$ref` keyword.

`#[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`. schemars_derive-1.2.1/deriving.md000064400000000000000000000017231046102023000151240ustar 00000000000000# 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); } ``` schemars_derive-1.2.1/src/ast/from_serde.rs000064400000000000000000000052541046102023000170470ustar 00000000000000use super::*; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::Ctxt; pub trait FromSerde: Sized { type SerdeType; fn from_serde(errors: &Ctxt, serde: Self::SerdeType) -> Self; fn vec_from_serde(errors: &Ctxt, serdes: Vec) -> Vec { serdes .into_iter() .map(|s| Self::from_serde(errors, s)) .collect() } } impl<'a> FromSerde for Container<'a> { type SerdeType = serde_ast::Container<'a>; fn from_serde(errors: &Ctxt, serde: Self::SerdeType) -> Self { let data = Data::from_serde(errors, serde.data); let attrs = ContainerAttrs::new(&serde.original.attrs, &data, errors); let rename_type_params = match &attrs.rename_format_string { Some(s) => crate::name::get_rename_format_type_params(errors, s, serde.generics), None => BTreeSet::new(), }; let mut cont = Self { ident: serde.ident, serde_attrs: serde.attrs, data, attrs, rename_type_params, // `relevant_type_params` populated in find_trait_bounds() relevant_type_params: BTreeSet::new(), // `generics.where_clause` extended in find_trait_bounds() generics: serde.generics.clone(), }; crate::bound::find_trait_bounds(serde.generics, &mut cont); cont } } impl<'a> FromSerde for Data<'a> { type SerdeType = serde_ast::Data<'a>; fn from_serde(errors: &Ctxt, serde: Self::SerdeType) -> Self { match serde { serde_ast::Data::Enum(variants) => { Data::Enum(Variant::vec_from_serde(errors, variants)) } serde_ast::Data::Struct(style, fields) => { Data::Struct(style, Field::vec_from_serde(errors, fields)) } } } } impl<'a> FromSerde for Variant<'a> { type SerdeType = serde_ast::Variant<'a>; fn from_serde(errors: &Ctxt, serde: Self::SerdeType) -> Self { Self { ident: serde.ident, serde_attrs: serde.attrs, style: serde.style, fields: Field::vec_from_serde(errors, serde.fields), original: serde.original, attrs: VariantAttrs::new(&serde.original.attrs, errors), } } } impl<'a> FromSerde for Field<'a> { type SerdeType = serde_ast::Field<'a>; fn from_serde(errors: &Ctxt, serde: Self::SerdeType) -> Self { Self { member: serde.member, serde_attrs: serde.attrs, ty: serde.ty, original: serde.original, attrs: FieldAttrs::new(&serde.original.attrs, errors), } } } schemars_derive-1.2.1/src/ast/mod.rs000064400000000000000000000122061046102023000154740ustar 00000000000000mod from_serde; use crate::attr::{ContainerAttrs, FieldAttrs, VariantAttrs}; use crate::idents::{GENERATOR, SCHEMA}; use crate::schema_exprs::SchemaExpr; use from_serde::FromSerde; use proc_macro2::TokenStream; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::{Ctxt, Derive}; use std::collections::BTreeSet; pub struct Container<'a> { pub ident: syn::Ident, pub serde_attrs: serde_derive_internals::attr::Container, pub data: Data<'a>, pub generics: syn::Generics, pub attrs: ContainerAttrs, /// A set of type params that are used in a `rename` attribute format string, e.g. `T` and `U` /// in `#[schemars(rename = "StructFor{T}And{U}")]`. This does not include const params. pub rename_type_params: BTreeSet<&'a syn::Ident>, /// A set of type params that are "relevant" to the impl, i.e. excluding params only used in /// `PhantomData` or skipped fields pub relevant_type_params: BTreeSet<&'a syn::Ident>, } pub enum Data<'a> { Enum(Vec>), Struct(serde_ast::Style, Vec>), } pub struct Variant<'a> { pub ident: syn::Ident, pub serde_attrs: serde_derive_internals::attr::Variant, pub style: serde_ast::Style, pub fields: Vec>, pub original: &'a syn::Variant, pub attrs: VariantAttrs, } pub struct Field<'a> { pub member: syn::Member, pub serde_attrs: serde_derive_internals::attr::Field, pub ty: &'a syn::Type, pub original: &'a syn::Field, pub attrs: FieldAttrs, } impl<'a> Container<'a> { 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(()) .map(|serde| Self::from_serde(&ctxt, serde)); ctxt.check() .map(|()| result.expect("from_ast set no errors on Ctxt, so should have returned Ok")) } pub fn transparent_field(&'a self) -> Option<&'a Field<'a>> { if self.serde_attrs.transparent() { if let Data::Struct(_, fields) = &self.data { return fields.iter().find(|f| f.serde_attrs.transparent()); } } None } pub fn add_mutators(&self, expr: &mut SchemaExpr) { self.attrs.common.add_mutators(expr); } pub fn name(&'a self) -> std::borrow::Cow<'a, str> { if self.attrs.rename_format_string.is_none() { if let Some(remote_name) = self.serde_attrs.remote().and_then(|r| r.segments.last()) { return remote_name.ident.to_string().into(); } } self.serde_attrs.name().deserialize_name().into() } } impl Variant<'_> { pub fn name(&self) -> Name<'_> { Name(self.serde_attrs.name()) } pub fn is_unit(&self) -> bool { matches!(self.style, serde_ast::Style::Unit) } pub fn add_mutators(&self, expr: &mut SchemaExpr) { self.attrs.common.add_mutators(expr); } pub fn with_contract_check(&self, action: TokenStream) -> TokenStream { with_contract_check( self.serde_attrs.skip_deserializing(), self.serde_attrs.skip_serializing(), action, ) } } impl Field<'_> { pub fn name(&self) -> Name<'_> { Name(self.serde_attrs.name()) } pub fn add_mutators(&self, expr: &mut SchemaExpr) { self.attrs.common.add_mutators(expr); self.attrs.validation.add_mutators(expr); if self.serde_attrs.skip_deserializing() { expr.mutators.push(quote! { #SCHEMA.insert("readOnly".into(), true.into()); }); } if self.serde_attrs.skip_serializing() { expr.mutators.push(quote! { #SCHEMA.insert("writeOnly".into(), true.into()); }); } } pub fn with_contract_check(&self, action: TokenStream) -> TokenStream { with_contract_check( self.serde_attrs.skip_deserializing(), self.serde_attrs.skip_serializing(), action, ) } } pub struct Name<'a>(&'a serde_derive_internals::attr::Name); impl quote::ToTokens for Name<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let ser_name = self.0.serialize_name(); let de_name = self.0.deserialize_name(); if ser_name == de_name { ser_name.to_tokens(tokens); } else { quote! { if #GENERATOR.contract().is_serialize() { #ser_name } else { #de_name } } .to_tokens(tokens); } } } fn with_contract_check( skip_deserializing: bool, skip_serializing: bool, action: TokenStream, ) -> TokenStream { match (skip_deserializing, skip_serializing) { (true, true) => TokenStream::new(), (true, false) => quote! { if #GENERATOR.contract().is_serialize() { #action } }, (false, true) => quote! { if #GENERATOR.contract().is_deserialize() { #action } }, (false, false) => action, } } schemars_derive-1.2.1/src/attr/custom_meta.rs000064400000000000000000000030441046102023000174200ustar 00000000000000use quote::ToTokens; use syn::{parse::Parse, Meta, MetaList, MetaNameValue, Path}; // An extended copy of `syn::Meta` with an additional `Not` variant #[derive(Clone)] pub enum CustomMeta { Path(Path), List(MetaList), NameValue(MetaNameValue), Not(Token![!], Path), } impl ToTokens for CustomMeta { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { CustomMeta::Not(not, path) => { not.to_tokens(tokens); path.to_tokens(tokens); } CustomMeta::Path(meta) => meta.to_tokens(tokens), CustomMeta::List(meta) => meta.to_tokens(tokens), CustomMeta::NameValue(meta) => meta.to_tokens(tokens), } } } impl From for CustomMeta { fn from(value: Meta) -> Self { match value { Meta::Path(meta) => Self::Path(meta), Meta::List(meta) => Self::List(meta), Meta::NameValue(meta) => Self::NameValue(meta), } } } impl Parse for CustomMeta { fn parse(input: syn::parse::ParseStream) -> syn::Result { Ok(if input.peek(Token![!]) { Self::Not(input.parse()?, input.parse()?) } else { Meta::parse(input)?.into() }) } } impl CustomMeta { pub fn path(&self) -> &Path { match self { CustomMeta::Not(_not, path) => path, CustomMeta::Path(path) => path, CustomMeta::List(meta) => &meta.path, CustomMeta::NameValue(meta) => &meta.path, } } } schemars_derive-1.2.1/src/attr/doc.rs000064400000000000000000000016351046102023000156510ustar 00000000000000use proc_macro2::TokenStream; use quote::ToTokens; use syn::{Attribute, Expr, ExprLit, Lit}; pub fn get_doc(attrs: &[Attribute]) -> Option { let mut macro_args: TokenStream = TokenStream::new(); for (i, line) in attrs .iter() .filter(|a| a.path().is_ident("doc")) .flat_map(|a| a.meta.require_name_value()) .enumerate() { if i > 0 { macro_args.extend([quote!(, "\n",)]); } if let Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) = &line.value { if let Some(trimmed) = lit_str.value().strip_prefix(' ') { trimmed.to_tokens(&mut macro_args); continue; } } line.value.to_tokens(&mut macro_args); } if macro_args.is_empty() { None } else { Some(parse_quote!(::core::concat!(#macro_args))) } } schemars_derive-1.2.1/src/attr/mod.rs000064400000000000000000000444711046102023000156700ustar 00000000000000mod custom_meta; mod doc; mod parse_meta; mod schemars_to_serde; mod validation; use parse_meta::{ parse_extensions, parse_name_value_expr, parse_name_value_lit_str, require_name_value_lit_str, require_path_only, }; use proc_macro2::TokenStream; use quote::ToTokens; use serde_derive_internals::Ctxt; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{Attribute, Expr, ExprLit, Ident, Lit, LitStr, Path, Type}; use validation::ValidationAttrs; use crate::ast::Data; use crate::idents::SCHEMA; use crate::schema_exprs::SchemaExpr; pub use custom_meta::*; pub use schemars_to_serde::process_serde_attrs; #[derive(Default)] pub struct CommonAttrs { pub doc: Option, pub deprecated: bool, pub title: Option, pub description: Option, pub examples: Vec, pub extensions: Vec<(String, TokenStream)>, pub transforms: Vec, } #[derive(Default)] pub struct FieldAttrs { pub common: CommonAttrs, pub with: Option, pub validation: ValidationAttrs, } #[derive(Default)] pub struct ContainerAttrs { pub common: CommonAttrs, pub repr: Option, pub crate_name: Option, // The actual parsing of this is done in `get_rename_format_type_params()`, // because it depends on the type's generic params. pub rename_format_string: Option, pub inline: bool, pub ref_variants: bool, pub with: Option, } #[derive(Default)] pub struct VariantAttrs { pub common: CommonAttrs, pub with: Option, } pub enum WithAttr { Type(Type), Function(Path), } 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")); } 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: CustomMeta, meta_name: &str, cx: &AttrCtxt, ) -> Result<(), CustomMeta> { match meta_name { "title" => match self.title { Some(_) => cx.duplicate_error(&meta), None => self.title = parse_name_value_expr(meta, cx).ok(), }, "description" => match self.description { Some(_) => cx.duplicate_error(&meta), None => self.description = parse_name_value_expr(meta, cx).ok(), }, "example" => { 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() { let lit_str_value = lit_str.value(); cx.error_spanned_by(&expr, format_args!( "`example` value must be an expression, and string literals that may be interpreted as function paths are currently disallowed to avoid migration errors \ (this restriction may be relaxed in a future version of schemars).\n\ If you want to use the result of a function, use `#[schemars(example = {lit_str_value}())]`.\n\ Or to use the string literal value, use `#[schemars(example = &\"{lit_str_value}\")]`.")); } } self.examples.push(expr); } } "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)); } } } "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); } } } _ => return Err(meta), } Ok(()) } pub 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() ) } pub fn add_mutators(&self, expr: &mut SchemaExpr) { let mutators = &mut expr.mutators; 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 { title.get_or_insert_with(|| { quote!({ const TITLE: &str = schemars::_private::get_title_and_description(#doc).0; TITLE }) }); description.get_or_insert_with(|| { quote!({ const DESCRIPTION: &str = schemars::_private::get_title_and_description(#doc).1; DESCRIPTION }) }); } 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); }); } if self.deprecated { mutators.push(quote! { #SCHEMA.insert("deprecated".into(), true.into()); }); } if !self.examples.is_empty() { let examples = self.examples.iter().map(|eg| { quote! { schemars::_private::serde_json::value::to_value(#eg) } }); mutators.push(quote! { #SCHEMA.insert("examples".into(), schemars::_private::serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect())); }); } // Post-Mutators - extensions and transforms will be applied after all other mutators for (k, v) in &self.extensions { expr.post_mutators.push(quote! { #SCHEMA.insert(#k.into(), schemars::_private::serde_json::json!(#v)); }); } for transform in &self.transforms { expr.post_mutators.push(quote_spanned! {transform.span()=> schemars::transform::Transform::transform(&mut #transform, &mut #SCHEMA); }); } } } impl FieldAttrs { 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"); 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); } 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: CustomMeta, meta_name: &str, cx: &AttrCtxt, ) -> Result<(), CustomMeta> { 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 Err(meta), } Ok(()) } pub fn is_default(&self) -> bool { matches!( self, Self { common, validation, with: None, } if common.is_default() && validation.is_default()) } } impl ContainerAttrs { pub fn new(attrs: &[Attribute], data: &Data, cx: &Ctxt) -> Self { let mut result = Self::default(); result.populate(attrs, data, cx); result } fn populate(&mut self, attrs: &[Attribute], data: &Data, 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(data, schemars_cx); self.process_attr(data, 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, data: &Data, cx: &mut AttrCtxt) { cx.parse_meta(|m, n, c| self.process_meta(m, n, data, c)); } fn process_meta( &mut self, meta: CustomMeta, meta_name: &str, data: &Data, cx: &AttrCtxt, ) -> Result<(), CustomMeta> { 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(), }, "rename" if cx.attr_type == "schemars" => match self.rename_format_string { Some(_) => cx.duplicate_error(&meta), None => self.rename_format_string = require_name_value_lit_str(meta, cx).ok(), }, "inline" => { if self.inline { cx.duplicate_error(&meta); } else if require_path_only(&meta, cx).is_ok() { self.inline = true; } } "with" if cx.attr_type == "schemars" => 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); } }, "_unstable_ref_variants" if cx.attr_type == "schemars" => { if !matches!(data, Data::Enum(_)) { cx.error_spanned_by( meta.path(), "`_unstable_ref_variants` can only be used on enums", ); } else if self.ref_variants { cx.duplicate_error(&meta); } else if require_path_only(&meta, cx).is_ok() { self.ref_variants = true; } } _ => return Err(meta), } Ok(()) } } 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: CustomMeta, meta_name: &str, cx: &AttrCtxt, ) -> Result<(), CustomMeta> { 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 Err(meta), } Ok(()) } pub fn is_default(&self) -> bool { matches!( self, Self { common, with: None, } if common.is_default() ) } } 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 path_str(path: &Path) -> String { path.get_ident().map_or_else( || path.into_token_stream().to_string().replace(' ', ""), Ident::to_string, ) } pub struct AttrCtxt<'a> { inner: &'a Ctxt, attr_type: &'static str, metas: Vec, } 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(CustomMeta, &str, &Self) -> Result<(), CustomMeta>, ) { let metas = std::mem::take(&mut self.metas); self.metas = metas .into_iter() .filter_map(|meta| match meta.path().get_ident().map(Ident::to_string) { Some(ident) => handle(meta, &ident, self).err(), _ => Some(meta), }) .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: &CustomMeta, 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: &CustomMeta) { if self.attr_type == "schemars" { self.error_spanned_by( meta, format_args!( "duplicate schemars attribute item `{}`", path_str(meta.path()) ), ); } } } impl Drop for AttrCtxt<'_> { fn drop(&mut self) { if self.attr_type == "schemars" { for unhandled_meta in self.metas.iter().filter(|m| !is_schemars_serde_keyword(m)) { self.error_spanned_by( unhandled_meta.path(), format_args!( "unknown schemars attribute `{}`", path_str(unhandled_meta.path()) ), ); } } } } fn is_schemars_serde_keyword(meta: &CustomMeta) -> bool { let known_keywords = schemars_to_serde::SCHEMARS_KEYWORDS_PARSED_BY_SERDE; meta.path() .get_ident() .is_some_and(|i| known_keywords.contains(&i.to_string().as_str())) } schemars_derive-1.2.1/src/attr/parse_meta.rs000064400000000000000000000263101046102023000172210ustar 00000000000000use proc_macro2::{TokenStream, TokenTree}; use syn::{ parse::{Parse, ParseStream, Parser}, punctuated::Punctuated, Expr, ExprLit, Lit, LitStr, MetaNameValue, }; use super::{path_str, AttrCtxt, CustomMeta}; pub fn require_path_only(meta: &CustomMeta, cx: &AttrCtxt) -> Result<(), ()> { let error_args = || { format!( "unexpected value of {} {} attribute item", cx.attr_type, path_str(meta.path()) ) }; match &meta { CustomMeta::Path(_) => Ok(()), CustomMeta::List(meta) => { cx.syn_error(syn::Error::new(meta.delimiter.span().join(), error_args())); Err(()) } CustomMeta::NameValue(meta) => { let eq_token = &meta.eq_token; let value = &meta.value; cx.error_spanned_by(quote!(#eq_token #value), error_args()); Err(()) } CustomMeta::Not(..) => { // Validation of "unset" attributes is currently done in schemars_to_serde Err(()) } } } pub fn parse_name_value_expr(meta: CustomMeta, cx: &AttrCtxt) -> Result { if let CustomMeta::NameValue(m) = meta { Ok(m.value) } else { 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 require_name_value_lit_str(meta: CustomMeta, cx: &AttrCtxt) -> Result { if let CustomMeta::NameValue(MetaNameValue { value: Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }), .. }) = meta { Ok(lit_str) } 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 ), ); Err(()) } } pub fn parse_name_value_lit_str(meta: CustomMeta, cx: &AttrCtxt) -> Result { let lit_str = require_name_value_lit_str(meta, cx)?; 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: &CustomMeta, cx: &AttrCtxt, ) -> Result, ()> { let parser = Punctuated::::parse_terminated; parse_meta_list_with(meta, cx, parser) } pub fn parse_length_or_range(outer_meta: &CustomMeta, 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: &CustomMeta, cx: &AttrCtxt) -> Result { parse_meta_list_with(meta, cx, Expr::parse) } pub fn parse_schemars_regex(outer_meta: &CustomMeta, cx: &AttrCtxt) -> Result { let mut pattern = None; for nested_meta in parse_nested_meta(outer_meta, 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: &CustomMeta, cx: &AttrCtxt) -> Result { let mut path = None; for nested_meta in parse_nested_meta(outer_meta, 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: CustomMeta, cx: &AttrCtxt) -> Result { 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 CustomMeta::Path) // `foo(bar)` => Expr (not CustomMeta::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: &CustomMeta, cx: &AttrCtxt, ) -> Result, ()> { let parser = Punctuated::::parse_terminated; parse_meta_list_with(meta, cx, parser) } fn parse_meta_list_with( meta: &CustomMeta, cx: &AttrCtxt, parser: F, ) -> Result { let CustomMeta::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: CustomMeta, 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(Default)] pub struct LengthOrRange { pub min: Option, pub max: Option, pub equal: Option, } 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, }) } } schemars_derive-1.2.1/src/attr/schemars_to_serde.rs000064400000000000000000000170371046102023000206000ustar 00000000000000use quote::ToTokens; use serde_derive_internals::Ctxt; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet}; use syn::parse::Parser; use syn::{Attribute, Data, Field, Variant}; use super::{get_meta_items, CustomMeta}; // 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", "untagged", "default", "skip", "skip_serializing", "skip_serializing_if", "skip_deserializing", "flatten", "remote", "transparent", "into", "from", "try_from", // 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. "serialize_with", "with", ]; pub(crate) static SCHEMARS_KEYWORDS_PARSED_BY_SERDE: &[&str] = // exclude "serialize_with" and "with" SERDE_KEYWORDS.split_at(SERDE_KEYWORDS.len() - 2).0; // 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) -> syn::Result<()> { let ctxt = Ctxt::new(); process_attrs(&ctxt, &mut input.attrs); match &mut input.data { Data::Struct(s) => process_serde_field_attrs(&ctxt, s.fields.iter_mut()), Data::Enum(e) => process_serde_variant_attrs(&ctxt, e.variants.iter_mut()), Data::Union(u) => process_serde_field_attrs(&ctxt, u.fields.named.iter_mut()), } ctxt.check() } fn process_serde_variant_attrs<'a>(ctxt: &Ctxt, variants: impl Iterator) { for v in variants { process_attrs(ctxt, &mut v.attrs); process_serde_field_attrs(ctxt, v.fields.iter_mut()); } } fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator) { for f in fields { process_attrs(ctxt, &mut f.attrs); } } fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { // 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 = other_attrs; let mut effective_serde_meta = Vec::new(); let mut unset_meta = BTreeMap::new(); let mut serde_meta_names = BTreeSet::new(); let mut schemars_meta_names = BTreeSet::new(); // Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes for meta in get_meta_items(attrs, "schemars", ctxt) { let Some(keyword) = get_meta_ident(&meta) else { continue; }; if matches!(meta, CustomMeta::Not(..)) { match unset_meta.entry(keyword) { Entry::Occupied(o) => { ctxt.error_spanned_by( meta, format_args!("duplicate schemars attribute item `!{}`", o.key()), ); } Entry::Vacant(v) => { v.insert(meta); } } } else if SCHEMARS_KEYWORDS_PARSED_BY_SERDE.contains(&keyword.as_ref()) { schemars_meta_names.insert(keyword); effective_serde_meta.push(meta); } } for (keyword, meta) in &unset_meta { if schemars_meta_names.contains(keyword) { ctxt.error_spanned_by( meta, format_args!("schemars attribute cannot contain both `{keyword}` and `!{keyword}`"), ); } } if schemars_meta_names.contains("skip") { schemars_meta_names.insert("skip_serializing".to_string()); schemars_meta_names.insert("skip_deserializing".to_string()); } // Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes for meta in get_meta_items(&serde_attrs, "serde", ctxt) { let Some(keyword) = get_meta_ident(&meta) else { continue; }; if !schemars_meta_names.contains(&keyword) && !unset_meta.contains_key(&keyword) && SERDE_KEYWORDS.contains(&keyword.as_ref()) && keyword != "bound" { effective_serde_meta.push(meta); } serde_meta_names.insert(keyword); } for (keyword, meta) in &unset_meta { if !serde_meta_names.contains(keyword) { ctxt.error_spanned_by( meta, format_args!( "useless `!{keyword}` - no serde attribute containing `{keyword}` is present" ), ); } } if !effective_serde_meta.is_empty() { let new_serde_attr = quote! { #[serde(#(#effective_serde_meta),*)] }; let parser = Attribute::parse_outer; match parser.parse2(new_serde_attr) { Ok(ref mut parsed) => attrs.append(parsed), Err(e) => ctxt.error_spanned_by(to_tokens(attrs), e), } } } fn to_tokens(attrs: &[Attribute]) -> impl ToTokens { let mut tokens = proc_macro2::TokenStream::new(); for attr in attrs { attr.to_tokens(&mut tokens); } tokens } fn get_meta_ident(meta: &CustomMeta) -> Option { meta.path().get_ident().map(std::string::ToString::to_string) } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use syn::DeriveInput; #[test] fn test_process_serde_attrs() { let mut input: DeriveInput = parse_quote! { #[serde(rename(serialize = "ser_name"), rename_all = "camelCase", from = "T")] #[serde(default, unknown_word)] #[schemars(rename = "overriden", another_unknown_word, !from)] #[misc] struct MyStruct { /// blah blah blah #[serde(skip_serializing_if = "some_fn", bound = "removed")] field1: i32, #[serde(serialize_with = "se", deserialize_with = "de")] #[schemars(with = "with", bound = "bound")] field2: i32, #[schemars(skip)] #[serde(skip_serializing)] field3: i32, } }; let expected: DeriveInput = parse_quote! { #[schemars(rename = "overriden", another_unknown_word, !from)] #[misc] #[serde(rename = "overriden", rename_all = "camelCase", default)] struct MyStruct { #[doc = r" blah blah blah"] #[serde(skip_serializing_if = "some_fn")] field1: i32, #[schemars(with = "with", bound = "bound")] #[serde(bound = "bound", serialize_with = "se")] field2: i32, #[schemars(skip)] #[serde(skip)] field3: i32, } }; if let Err(e) = process_serde_attrs(&mut input) { panic!("process_serde_attrs returned error: {e}") } assert_eq!(input, expected); } } schemars_derive-1.2.1/src/attr/validation.rs000064400000000000000000000206751046102023000172430ustar 00000000000000use proc_macro2::TokenStream; use syn::Expr; use crate::{idents::SCHEMA, schema_exprs::SchemaExpr}; 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, CustomMeta, }; #[derive(Clone, Copy, PartialEq)] pub enum Format { Email, Uri, Ip, Ipv4, Ipv6, } impl Format { fn attr_str(self) -> &'static str { match self { Format::Email => "email", Format::Uri => "url", Format::Ip => "ip", Format::Ipv4 => "ipv4", Format::Ipv6 => "ipv6", } } fn schema_str(self) -> &'static str { match self { Format::Email => "email", Format::Uri => "uri", 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(Default)] pub struct ValidationAttrs { 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 add_mutators(&self, expr: &mut SchemaExpr) { self.add_mutators2(&mut expr.mutators, "e!(&mut #SCHEMA)); } 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); } if let Some(range) = &self.range { Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema); } 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()); }); } if let Some(contains) = &self.contains { mutators.push(quote! { schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string()); }); } if let Some(format) = &self.format { let f = format.schema_str(); mutators.push(quote! { (#mut_ref_schema).insert("format".into(), #f.into()); }); } 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)* }); }); } } } 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(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); }); } } 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); } 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: CustomMeta, meta_name: &str, cx: &AttrCtxt, ) -> Result<(), CustomMeta> { if let Some(format) = Format::from_attr_str(meta_name) { self.handle_format(&meta, format, cx); return Ok(()); } 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; } } "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(), } } "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(); } } } } "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); } } _ => return Err(meta), } Ok(()) } fn handle_format(&mut self, meta: &CustomMeta, 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); } } } } pub(crate) fn is_default(&self) -> bool { matches!( self, Self { contains: None, format: None, length: None, range: None, pattern: None, regex: None, required: false, inner: None, } ) } } schemars_derive-1.2.1/src/bound.rs000064400000000000000000000242641046102023000152440ustar 00000000000000use crate::{ ast::{Container, Data, Field, Variant}, attr::WithAttr, }; use std::collections::BTreeSet; use syn::{punctuated::Punctuated, Ident}; // This logic is heavily based on serde_derive: // https://github.com/serde-rs/serde/blob/a1ddb18c92f32d64b2ccaf31ddd776e56be34ba2/serde_derive/src/bound.rs#L91 pub fn find_trait_bounds<'a>(orig_generics: &'a syn::Generics, cont: &mut Container<'a>) { if orig_generics.params.is_empty() { return; } let all_type_params = orig_generics .type_params() .map(|param| ¶m.ident) .collect(); assert!(cont.rename_type_params.is_subset(&all_type_params)); let mut visitor = FindTyParams { all_type_params, relevant_type_params: cont.rename_type_params.clone(), type_params_for_bound: cont.rename_type_params.clone(), }; let mut field_explicit_bounds = Vec::new(); if visitor.all_type_params.len() > visitor.relevant_type_params.len() { match &cont.data { Data::Enum(variants) => { for variant in variants { let relevant_fields = variant .fields .iter() .filter(|field| needs_jsonschema_bound(field, Some(variant))); for field in relevant_fields { field_explicit_bounds.extend(field.serde_attrs.de_bound()); visitor.visit_field(field); } } } Data::Struct(_, fields) => { let relevant_fields = fields .iter() .filter(|field| needs_jsonschema_bound(field, None)); for field in relevant_fields { field_explicit_bounds.extend(field.serde_attrs.de_bound()); visitor.visit_field(field); } } } } cont.relevant_type_params = visitor.relevant_type_params; let where_clause = cont.generics.make_where_clause(); if let Some(bounds) = cont.serde_attrs.de_bound() { where_clause.predicates.extend(bounds.iter().cloned()); } else { where_clause .predicates .extend(visitor.type_params_for_bound.into_iter().map(|ty| { syn::WherePredicate::Type(syn::PredicateType { lifetimes: None, bounded_ty: syn::Type::Path(syn::TypePath { qself: None, path: syn::Path { leading_colon: None, segments: Punctuated::from_iter([syn::PathSegment { ident: (*ty).clone(), arguments: syn::PathArguments::None, }]), }, }), colon_token: ::default(), bounds: Punctuated::from_iter([syn::TypeParamBound::Trait(syn::TraitBound { paren_token: None, modifier: syn::TraitBoundModifier::None, lifetimes: None, path: parse_quote!(schemars::JsonSchema), })]), }) })); } where_clause .predicates .extend(field_explicit_bounds.into_iter().flatten().cloned()); } fn needs_jsonschema_bound(field: &Field, variant: Option<&Variant>) -> bool { if let Some(variant) = variant { if variant.serde_attrs.skip_deserializing() && variant.serde_attrs.skip_serializing() { return false; } } if field.serde_attrs.skip_deserializing() && field.serde_attrs.skip_serializing() { return false; } true } struct FindTyParams<'ast> { all_type_params: BTreeSet<&'ast Ident>, relevant_type_params: BTreeSet<&'ast Ident>, type_params_for_bound: BTreeSet<&'ast Ident>, } #[allow(clippy::single_match)] impl FindTyParams<'_> { fn visit_field(&mut self, field: &Field) { match &field.attrs.with { Some(WithAttr::Type(ty)) => self.visit_type(field, ty), Some(WithAttr::Function(_)) => { // `schema_with` function type params may or may not implement `JsonSchema` } None => self.visit_type(field, &field.original.ty), } } fn visit_path(&mut self, field: &Field, path: &syn::Path) { if let Some(seg) = path.segments.last() { if seg.ident == "PhantomData" { // Hardcoded exception, because PhantomData implements // JsonSchema whether or not T implements it. return; } } if path.leading_colon.is_none() { if let Some(first_segment) = path.segments.first() { let id = &first_segment.ident; if let Some(id) = self.all_type_params.get(id) { self.relevant_type_params.insert(id); if field.serde_attrs.de_bound().is_none() { self.type_params_for_bound.insert(id); } } } } for segment in &path.segments { self.visit_path_segment(field, segment); } } fn visit_type(&mut self, field: &Field, ty: &syn::Type) { match ty { syn::Type::Array(ty) => self.visit_type(field, &ty.elem), syn::Type::BareFn(ty) => { for arg in &ty.inputs { self.visit_type(field, &arg.ty); } self.visit_return_type(field, &ty.output); } syn::Type::Group(ty) => self.visit_type(field, &ty.elem), syn::Type::ImplTrait(ty) => { for bound in &ty.bounds { self.visit_type_param_bound(field, bound); } } syn::Type::Macro(ty) => self.visit_macro(field, &ty.mac), syn::Type::Paren(ty) => self.visit_type(field, &ty.elem), syn::Type::Path(ty) => { if let Some(qself) = &ty.qself { self.visit_type(field, &qself.ty); } self.visit_path(field, &ty.path); } syn::Type::Ptr(ty) => self.visit_type(field, &ty.elem), syn::Type::Reference(ty) => { self.visit_type(field, &ty.elem); } syn::Type::Slice(ty) => self.visit_type(field, &ty.elem), syn::Type::TraitObject(ty) => { for bound in &ty.bounds { self.visit_type_param_bound(field, bound); } } syn::Type::Tuple(ty) => { for elem in &ty.elems { self.visit_type(field, elem); } } _ => {} } } fn visit_path_segment(&mut self, field: &Field, segment: &syn::PathSegment) { self.visit_path_arguments(field, &segment.arguments); } fn visit_path_arguments(&mut self, field: &Field, arguments: &syn::PathArguments) { match arguments { syn::PathArguments::None => {} syn::PathArguments::AngleBracketed(arguments) => { for arg in &arguments.args { match arg { syn::GenericArgument::Type(arg) => self.visit_type(field, arg), syn::GenericArgument::AssocType(arg) => self.visit_type(field, &arg.ty), _ => {} } } } syn::PathArguments::Parenthesized(arguments) => { for argument in &arguments.inputs { self.visit_type(field, argument); } self.visit_return_type(field, &arguments.output); } } } fn visit_return_type(&mut self, field: &Field, return_type: &syn::ReturnType) { match return_type { syn::ReturnType::Default => {} syn::ReturnType::Type(_, output) => self.visit_type(field, output), } } fn visit_type_param_bound(&mut self, field: &Field, bound: &syn::TypeParamBound) { match bound { syn::TypeParamBound::Trait(bound) => self.visit_path(field, &bound.path), _ => {} } } // Type parameter should not be considered used by a macro path. // // struct TypeMacro { // mac: T!(), // marker: PhantomData, // } #[allow(clippy::unused_self)] fn visit_macro(&mut self, _field: &Field, _mac: &syn::Macro) {} } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn test_enum_bounds() { // All type params should be included in `JsonSchema` trait bounds except `Z` let input = parse_quote! { #[schemars(rename = "MyEnum<{T}, {U}, {V}, {W}, {X}, {Y}, {{Z}}>")] pub enum MyEnum<'a, const LEN: usize, T, U, V, W, X, Y, Z> where X: Trait, Z: OtherTrait { A, B(), C(T), D(U, (i8, V, bool)), E { a: W, b: [&'a Option::AssocType::Z>>; LEN], c: Token![Z], d: PhantomData, #[serde(skip)] e: Z, }, #[serde(skip)] F(Z), } }; let cont = Container::from_ast(&input).unwrap(); assert_eq!( cont.generics.where_clause, Some(parse_quote!( where X: Trait, Z: OtherTrait, T: schemars::JsonSchema, U: schemars::JsonSchema, V: schemars::JsonSchema, W: schemars::JsonSchema, X: schemars::JsonSchema, Y: schemars::JsonSchema )) ); let relevant_type_params = Vec::from_iter(cont.relevant_type_params.into_iter().map(Ident::to_string)); assert_eq!(relevant_type_params, vec!["T", "U", "V", "W", "X", "Y"]); } } schemars_derive-1.2.1/src/idents.rs000064400000000000000000000007401046102023000154140ustar 00000000000000use proc_macro2::{Ident, Span, TokenStream}; 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(ident); } } schemars_derive-1.2.1/src/lib.rs000064400000000000000000000152771046102023000147070ustar 00000000000000#![forbid(unsafe_code)] #![deny(unused_imports, clippy::cargo, clippy::pedantic)] #![allow( clippy::result_large_err, clippy::wildcard_imports, clippy::from_iter_instead_of_collect, clippy::too_many_lines )] #[macro_use] extern crate quote; #[macro_use] extern crate syn; extern crate proc_macro; mod ast; mod attr; mod bound; mod idents; mod name; mod schema_exprs; use ast::Container; use idents::GENERATOR; use proc_macro2::TokenStream; use std::collections::BTreeSet; use syn::spanned::Spanned; #[doc = "Derive macro for `JsonSchema` trait."] #[cfg_attr(not(doctest), allow(clippy::needless_doctest_main), 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(syn::Error::into_compile_error) .into() } #[proc_macro_derive(JsonSchema_repr, attributes(schemars, serde))] 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(syn::Error::into_compile_error) .into() } fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result { attr::process_serde_attrs(&mut input)?; let cont = Container::from_ast(&input)?; let crate_alias = cont.attrs.crate_name.as_ref().map(|path| { quote_spanned! {path.span()=> use #path as schemars; } }); let type_name = &cont.ident; let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl(); if let Some(ty) = get_transparent_type(&cont) { return Ok(quote! { const _: () = { #crate_alias #[automatically_derived] impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause { fn inline_schema() -> bool { <#ty as schemars::JsonSchema>::inline_schema() } fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { <#ty as schemars::JsonSchema>::schema_name() } fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { <#ty as schemars::JsonSchema>::schema_id() } 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 { <#ty as schemars::JsonSchema>::_schemars_private_is_option() } }; }; }); } let name = cont.name(); let const_params = BTreeSet::from_iter(cont.generics.const_params().map(|c| &c.ident)); // We can't just check if `cont.rename_type_params` is empty, because even if it is, there may // be const params in the rename format string let schema_name = if cont.attrs.rename_format_string.is_none() || !name.contains('{') { quote! { schemars::_private::alloc::borrow::Cow::Borrowed(#name) } } else { let type_params = &cont.rename_type_params; quote! { schemars::_private::alloc::borrow::Cow::Owned(schemars::_private::alloc::format!( #name, #(#type_params=<#type_params as schemars::JsonSchema>::schema_name(),)* )) } }; let schema_id = if const_params.is_empty() && cont.relevant_type_params.is_empty() { quote! { schemars::_private::alloc::borrow::Cow::Borrowed(::core::concat!( ::core::module_path!(), "::", #name )) } } else { let relevant_type_params = &cont.relevant_type_params; let format_string_braces = vec!["{}"; const_params.len() + relevant_type_params.len()]; quote! { schemars::_private::alloc::borrow::Cow::Owned( schemars::_private::alloc::format!( ::core::concat!( ::core::module_path!(), "::{}<", #(#format_string_braces,)* ">" ), #name, #(#const_params,)* #(schemars::_schemars_maybe_schema_id!(#relevant_type_params),)* ) ) } }; let schema_expr = if repr { schema_exprs::expr_for_repr(&cont)? } else { schema_exprs::expr_for_container(&cont) }; let inline = cont.attrs.inline; Ok(quote! { const _: () = { #crate_alias #[automatically_derived] #[allow(unused_braces)] impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause { fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { #schema_name } fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { #schema_id } fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { #schema_expr } fn inline_schema() -> bool { #inline } }; }; }) } fn get_transparent_type<'a>(cont: &'a Container) -> Option<&'a syn::Type> { // If any schemars attributes for setting metadata (e.g. description) are present, then // it's not fully transparent, so use the normal `schema_exprs::expr_for_container` // implementation (which always treats the struct as a newtype if it has `transparent`) if let Some(attr::WithAttr::Type(ty)) = &cont.attrs.with { if cont.attrs.common.is_default() { return Some(ty); } } if let Some(transparent_field) = cont.transparent_field() { if cont.attrs.common.is_default() && transparent_field.attrs.is_default() { return Some(transparent_field.ty); } } None } schemars_derive-1.2.1/src/name.rs000064400000000000000000000046231046102023000150520ustar 00000000000000use serde_derive_internals::Ctxt; use std::collections::{BTreeMap, BTreeSet}; use syn::Ident; pub fn get_rename_format_type_params<'a>( errors: &Ctxt, rename_format_string: &syn::LitStr, generics: &'a syn::Generics, ) -> BTreeSet<&'a Ident> { let mut type_params = BTreeSet::new(); let mut str_value = rename_format_string.value(); if !str_value.contains('{') { return type_params; } if str_value.contains("{{") { str_value = str_value.replace("{{", ""); } if str_value.contains("}}") { str_value = str_value.replace("}}", ""); } let all_const_params = BTreeSet::from_iter(generics.const_params().map(|c| c.ident.to_string())); let all_type_params = BTreeMap::from_iter( generics .type_params() .map(|c| (c.ident.to_string(), &c.ident)), ); let mut segments = str_value.split('{'); if segments.next().unwrap_or_default().contains('}') { // The name format string contains a '}' before the first '{' errors.error_spanned_by( rename_format_string, "invalid name format string: unmatched `}` found", ); } for segment in segments { match segment.split_once('}') { Some((param, rest)) => { if rest.contains('}') { errors.error_spanned_by( rename_format_string, "invalid name format string: unmatched `}` found", ); } if let Some(type_param) = all_type_params.get(param) { type_params.insert(type_param); } else if all_const_params.contains(param) { // Any const params will be magically picked up from the surrounding scope by // `format!()` } else { errors.error_spanned_by( rename_format_string, format_args!( "invalid name format string: expected generic param, found `{param}`" ), ); } } None => { errors.error_spanned_by( rename_format_string, "invalid name format string: found `{` without matching `}`", ); } } } type_params } schemars_derive-1.2.1/src/schema_exprs.rs000064400000000000000000000663401046102023000166170ustar 00000000000000use 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 syn::spanned::Spanned; pub struct SchemaExpr { /// Definitions for types or functions that may be used within the creator or mutators pub definitions: Vec, /// An expression that produces a `Schema` pub creator: TokenStream, /// Statements (including terminating semicolon) that mutate a var `schema` of type `Schema` pub mutators: Vec, /// Same as `mutators`, but always applied last pub post_mutators: Vec, } impl From for SchemaExpr { fn from(creator: TokenStream) -> Self { Self { definitions: Vec::new(), creator, mutators: Vec::new(), post_mutators: Vec::new(), } } } impl ToTokens for SchemaExpr { fn to_tokens(&self, tokens: &mut TokenStream) { let Self { definitions, creator, mutators, post_mutators, } = self; tokens.extend(if mutators.is_empty() && post_mutators.is_empty() { quote!({ #(#definitions)* #creator }) } else { let mutators = mutators.iter().chain(post_mutators.iter()); quote!({ #(#definitions)* let mut #SCHEMA = #creator; #(#mutators)* #SCHEMA }) }); } } pub fn expr_for_container(cont: &Container) -> SchemaExpr { let type_from = cont .serde_attrs .type_from() .or(cont.serde_attrs.type_try_from()); let type_into = cont.serde_attrs.type_into(); let mut schema_expr = if let Some(with) = &cont.attrs.with { expr_for_container_with(cont, with) } else if let Some(transparent_field) = cont.transparent_field() { expr_for_newtype_struct(cont, transparent_field) } else if let (Some(from), Some(into)) = (type_from, type_into) { quote! { if #GENERATOR.contract().is_deserialize() { <#from as schemars::JsonSchema>::json_schema(#GENERATOR) } else { <#into as schemars::JsonSchema>::json_schema(#GENERATOR) } } .into() } else { let schema_expr = match &cont.data { Data::Struct(Style::Unit, _) => expr_for_unit_struct(), Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(cont, &fields[0]), Data::Struct(Style::Tuple, fields) => expr_for_tuple_struct(cont, fields), Data::Struct(Style::Struct, fields) => expr_for_struct( cont, fields, cont.serde_attrs.default(), cont.serde_attrs.deny_unknown_fields(), ), Data::Enum(variants) => expr_for_enum(cont, variants, &cont.serde_attrs), }; if let Some(from) = type_from { quote! { if #GENERATOR.contract().is_deserialize() { <#from as schemars::JsonSchema>::json_schema(#GENERATOR) } else { #schema_expr } } .into() } else if let Some(into) = type_into { quote! { if #GENERATOR.contract().is_serialize() { <#into as schemars::JsonSchema>::json_schema(#GENERATOR) } else { #schema_expr } } .into() } else { schema_expr } }; cont.add_mutators(&mut schema_expr); schema_expr } 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(), "JsonSchema_repr: missing #[repr(...)] attribute", ) })?; let Data::Enum(variants) = &cont.data else { 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_spanned( v.original, "JsonSchema_repr: must be a unit variant", )), }) { return Err(non_unit_error); } let enum_ident = &cont.ident; let variant_idents = variants.iter().map(|v| &v.ident); 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); Ok(schema_expr) } fn expr_for_container_with(cont: &Container, with_attr: &WithAttr) -> SchemaExpr { let (ty, type_def) = type_for_schema(cont, with_attr); let mut schema_expr = SchemaExpr::from(quote! { <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) }); schema_expr.definitions.extend(type_def); schema_expr } fn expr_for_field( cont: &Container, field: &Field, is_internal_tagged_enum_newtype: bool, ) -> SchemaExpr { let (ty, type_def) = type_for_field_schema(cont, field); let span = field.original.span(); let schema_expr = if field.attrs.validation.required { quote_spanned! {span=> <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } } else if is_internal_tagged_enum_newtype { quote_spanned! {span=> schemars::_private::json_schema_for_internally_tagged_enum_newtype_variant::<#ty>(#GENERATOR) } } else { quote_spanned! {span=> #GENERATOR.subschema_for::<#ty>() } }; let mut schema_expr = SchemaExpr::from(schema_expr); schema_expr.definitions.extend(type_def); field.add_mutators(&mut schema_expr); schema_expr } fn type_for_field_schema(cont: &Container, field: &Field) -> (syn::Type, Option) { match &field.attrs.with { None => (field.ty.clone(), None), Some(with_attr) => type_for_schema(cont, with_attr), } } fn type_for_schema(cont: &Container, with_attr: &WithAttr) -> (syn::Type, Option) { match with_attr { WithAttr::Type(ty) => (ty.clone(), None), WithAttr::Function(fun) => { let cont_name = &cont.ident; let fn_name = fun.segments.last().unwrap().ident.to_string(); let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl(); let type_def = quote_spanned! {fun.span()=> struct _SchemarsSchemaWithFunction(::core::marker::PhantomData); impl #impl_generics schemars::JsonSchema for _SchemarsSchemaWithFunction<#cont_name #ty_generics> #where_clause { fn inline_schema() -> bool { true } fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { schemars::_private::alloc::borrow::Cow::Borrowed(#fn_name) } fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { schemars::_private::alloc::borrow::Cow::Borrowed(::core::concat!( "_SchemarsSchemaWithFunction/", ::core::module_path!(), "/", ::core::stringify!(#fun) )) } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { #fun(generator) } } }; ( parse_quote!(_SchemarsSchemaWithFunction::<#cont_name #ty_generics>), Some(type_def), ) } } } fn expr_for_enum( cont: &Container, variants: &[Variant], cattrs: &serde_attr::Container, ) -> SchemaExpr { if variants.is_empty() { return quote!(schemars::Schema::from(false)).into(); } let deny_unknown_fields = cattrs.deny_unknown_fields(); let variants = variants.iter(); match cattrs.tag() { TagType::External => expr_for_external_tagged_enum(cont, variants, deny_unknown_fields), TagType::None => expr_for_untagged_enum(cont, variants, deny_unknown_fields), TagType::Internal { tag } => { expr_for_internal_tagged_enum(cont, variants, tag, deny_unknown_fields) } TagType::Adjacent { tag, content } => { expr_for_adjacent_tagged_enum(cont, variants, tag, content, deny_unknown_fields) } } } fn expr_for_external_tagged_enum<'a>( cont: &Container, variants: impl Iterator>, deny_unknown_fields: bool, ) -> SchemaExpr { let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = variants.partition(|v| { v.is_unit() && v.attrs.is_default() && !v.serde_attrs.untagged() && !cont.attrs.ref_variants }); let add_unit_names = unit_variants.iter().map(|v| { let name = v.name(); v.with_contract_check(quote! { enum_values.push((#name).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(); #(#add_unit_names)* enum_values }), ); schemars::Schema::from(map) })); if complex_variants.is_empty() { return unit_schema; } let mut schemas = Vec::new(); if !unit_variants.is_empty() { schemas.push((None, unit_schema)); } schemas.extend(complex_variants.into_iter().map(|variant| { if variant.serde_attrs.untagged() { return ( Some(variant), expr_for_untagged_enum_variant(cont, variant, deny_unknown_fields, true), ); } let name = variant.name(); 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(cont, variant, deny_unknown_fields, false); quote! { schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema) } }); variant.add_mutators(&mut schema_expr); (Some(variant), schema_expr) })); variant_subschemas(cont, true, schemas) } fn expr_for_internal_tagged_enum<'a>( cont: &Container, variants: impl Iterator>, tag_name: &str, deny_unknown_fields: bool, ) -> SchemaExpr { let variant_schemas = variants .map(|variant| { if variant.serde_attrs.untagged() { return (Some(variant), expr_for_untagged_enum_variant(cont, variant, deny_unknown_fields, true)) } let mut schema_expr = expr_for_internal_tagged_enum_variant(cont, variant, deny_unknown_fields); let name = variant.name(); schema_expr.mutators.push(quote!( schemars::_private::apply_internal_enum_variant_tag(&mut #SCHEMA, #tag_name, #name, #deny_unknown_fields); )); variant.add_mutators(&mut schema_expr); (Some(variant), schema_expr) }) .collect(); variant_subschemas(cont, true, variant_schemas) } fn expr_for_untagged_enum<'a>( cont: &Container, variants: impl Iterator>, deny_unknown_fields: bool, ) -> SchemaExpr { let schemas = variants .map(|variant| { let schema_expr = expr_for_untagged_enum_variant(cont, variant, deny_unknown_fields, true); (Some(variant), schema_expr) }) .collect(); // Untagged enums can easily have variants whose schemas overlap; rather // that checking the exclusivity of each subschema we simply us `any_of`. variant_subschemas(cont, false, schemas) } fn expr_for_adjacent_tagged_enum<'a>( cont: &Container, variants: impl Iterator>, tag_name: &str, content_name: &str, deny_unknown_fields: bool, ) -> SchemaExpr { let schemas = variants .map(|variant| { if variant.serde_attrs.untagged() { return ( Some(variant), expr_for_untagged_enum_variant(cont, variant, deny_unknown_fields, true), ); } let content_schema = if variant.is_unit() && variant.attrs.with.is_none() { None } else { Some(expr_for_untagged_enum_variant( cont, variant, deny_unknown_fields, false, )) }; let (add_content_to_props, add_content_to_required) = content_schema .map(|content_schema| { ( quote!(#content_name: (#content_schema),), quote!(#content_name,), ) }) .unwrap_or_default(); let name = variant.name(); let tag_schema = quote! { schemars::json_schema!({ "type": "string", "const": #name, }) }; let set_additional_properties = if deny_unknown_fields { quote! { "additionalProperties": false, } } else { TokenStream::new() }; 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.add_mutators(&mut outer_schema); (Some(variant), outer_schema) }) .collect(); variant_subschemas(cont, true, schemas) } /// Callers must determine if all subschemas are mutually exclusive. The current behaviour is to /// assume that variants are mutually exclusive except for untagged enums. fn variant_subschemas( cont: &Container, mut unique: bool, schemas: Vec<(Option<&Variant>, SchemaExpr)>, ) -> SchemaExpr { if schemas .iter() .any(|(v, _)| v.is_some_and(|v| v.serde_attrs.untagged())) { unique = false; } let keyword = if unique { "oneOf" } else { "anyOf" }; let add_schemas = schemas.into_iter().map(|(variant, mut schema)| { if cont.attrs.ref_variants { schema = enum_ref_variants(cont, variant, schema); } let add = quote! { enum_values.push(#schema.to_value()); }; match variant { Some(v) => v.with_contract_check(add), None => add, } }); 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(); #(#add_schemas)* enum_values }), ); schemars::Schema::from(map) }) .into() } fn enum_ref_variants(cont: &Container, variant: Option<&Variant>, expr: SchemaExpr) -> SchemaExpr { let Some(variant) = variant else { return expr; }; let cont_name = &cont.ident; // FIXME can this use serialize name where appropriate? let variant_name = variant.serde_attrs.name().deserialize_name(); let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl(); let type_def = quote! { struct _SchemarsRefVariant(::core::marker::PhantomData); impl #impl_generics schemars::JsonSchema for _SchemarsRefVariant<#cont_name #ty_generics> #where_clause { fn inline_schema() -> bool { false } fn schema_name() -> schemars::_private::alloc::borrow::Cow<'static, str> { schemars::_private::alloc::borrow::Cow::Borrowed(#variant_name) } fn schema_id() -> schemars::_private::alloc::borrow::Cow<'static, str> { schemars::_private::alloc::borrow::Cow::Owned( schemars::_private::alloc::format!( "_SchemarsRefVariant/{}::{}", <#cont_name #ty_generics as schemars::JsonSchema>::schema_id(), #variant_name, )) } fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { #expr } } }; let mut expr = SchemaExpr::from( quote!(#GENERATOR.subschema_for::<_SchemarsRefVariant::<#cont_name #ty_generics>>()), ); expr.definitions.push(type_def); expr } // This function is also used for tagged variants, in which case the resulting SchemaExpr will be // embedded or mutated to include the tag. `is_actually_untagged` will be true if the enum or the // variant has the `untagged` attribute. fn expr_for_untagged_enum_variant( cont: &Container, variant: &Variant, deny_unknown_fields: bool, is_actually_untagged: bool, ) -> SchemaExpr { let mut schema_expr = if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(cont, with_attr); let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> #GENERATOR.subschema_for::<#ty>() }); schema_expr.definitions.extend(type_def); schema_expr } else { match variant.style { Style::Unit => expr_for_unit_struct(), Style::Newtype => expr_for_field(cont, &variant.fields[0], false), Style::Tuple => expr_for_tuple_struct(cont, &variant.fields), Style::Struct => expr_for_struct( cont, &variant.fields, &SerdeDefault::None, deny_unknown_fields, ), } }; if is_actually_untagged { if variant.attrs.common.title.is_none() { let title = variant.name(); schema_expr.mutators.push(quote! { if #GENERATOR.settings().untagged_enum_variant_titles { #SCHEMA.insert("title".into(), #title.into()); } }); } variant.add_mutators(&mut schema_expr); } schema_expr } fn expr_for_internal_tagged_enum_variant( cont: &Container, variant: &Variant, deny_unknown_fields: bool, ) -> SchemaExpr { if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(cont, with_attr); let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) }); schema_expr.definitions.extend(type_def); return schema_expr; } match variant.style { Style::Unit => expr_for_unit_struct(), Style::Newtype => expr_for_field(cont, &variant.fields[0], true), Style::Tuple => expr_for_tuple_struct(cont, &variant.fields), Style::Struct => expr_for_struct( cont, &variant.fields, &SerdeDefault::None, deny_unknown_fields, ), } } fn expr_for_unit_struct() -> SchemaExpr { quote! { #GENERATOR.subschema_for::<()>() } .into() } fn expr_for_newtype_struct(cont: &Container, field: &Field) -> SchemaExpr { expr_for_field(cont, field, false) } fn expr_for_tuple_struct(cont: &Container, fields: &[Field]) -> SchemaExpr { let fields: Vec<_> = fields .iter() .map(|f| { let field_expr = expr_for_field(cont, f, false); f.with_contract_check(quote! { prefix_items.push((#field_expr).to_value()); }) }) .collect(); let max_len = fields.len(); // The length check must be done at runtime because it can vary based on the contract if any // items have `skip_serializing` or `skip_deserializing` attributes. quote!({ let mut prefix_items = schemars::_private::alloc::vec::Vec::::with_capacity(#max_len); #(#fields)* let len = prefix_items.len(); let mut map = schemars::_private::serde_json::Map::new(); map.insert("type".into(), "array".into()); if !prefix_items.is_empty() { map.insert("prefixItems".into(), prefix_items.into()); map.insert("minItems".into(), len.into()); } map.insert("maxItems".into(), len.into()); schemars::Schema::from(map) }) .into() } fn expr_for_struct( cont: &Container, fields: &[Field], default: &SerdeDefault, deny_unknown_fields: bool, ) -> SchemaExpr { let set_container_default = match default { SerdeDefault::None => None, SerdeDefault::Default => Some(quote!(let #STRUCT_DEFAULT = Self::default();)), SerdeDefault::Path(path) => Some(quote!(let #STRUCT_DEFAULT = #path();)), }; // a vec of mutators let properties: Vec = fields .iter() .filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing()) .map(|field| { if field.serde_attrs.flatten() { let (ty, type_def) = type_for_field_schema(cont, field); 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) }); schema_expr.definitions.extend(type_def); field.with_contract_check(quote! { schemars::_private::flatten(&mut #SCHEMA, #schema_expr); }) } else { let mut schema_expr = expr_for_field(cont, field, false); 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| #SCHEMA.insert("default".into(), d)); }); } let name = field.name(); let (ty, type_def) = type_for_field_schema(cont, field); if type_def.is_some() { assert!(!schema_expr.definitions.is_empty()); } let has_default = set_container_default.is_some() || !field.serde_attrs.default().is_none(); let has_skip_serialize_if = field.serde_attrs.skip_serializing_if().is_some(); let required_attr = field.attrs.validation.required; let is_optional = if has_skip_serialize_if && has_default { quote!(true) } else if !has_skip_serialize_if && !has_default && !required_attr { quote!(#GENERATOR.contract().is_deserialize() && <#ty as schemars::JsonSchema>::_schemars_private_is_option()) } else { quote!(if #GENERATOR.contract().is_deserialize() { #has_default || (!#required_attr && <#ty as schemars::JsonSchema>::_schemars_private_is_option()) } else { #has_skip_serialize_if }) }; // Embed definitions outside of `#schema_expr`, because they may contain the // definition of the `#ty` type which is used in `#is_optional`` let definitions = core::mem::take(&mut schema_expr.definitions); field.with_contract_check(quote!({ #(#definitions)* schemars::_private::insert_object_property(&mut #SCHEMA, #name, #is_optional, #schema_expr); })) } }) .collect(); let set_additional_properties = if deny_unknown_fields { quote! { "additionalProperties": false, } } else { TokenStream::new() }; SchemaExpr { definitions: set_container_default.into_iter().collect(), creator: quote!(schemars::json_schema!({ "type": "object", #set_additional_properties })), mutators: properties, post_mutators: Vec::new(), } } fn field_default_expr(field: &Field, container_has_default: bool) -> Option { let field_default = field.serde_attrs.default(); if field.serde_attrs.skip_serializing() || (field_default.is_none() && !container_has_default) { return None; } let ty = field.ty; let default_expr = match field_default { SerdeDefault::None => { let member = &field.member; quote!(#STRUCT_DEFAULT.#member) } SerdeDefault::Default => quote!(<#ty>::default()), SerdeDefault::Path(path) => quote!(#path()), }; let default_expr = if let Some(skip_if) = field.serde_attrs.skip_serializing_if() { quote! { { let default = #default_expr; if #skip_if(&default) { None } else { Some(default) } } } } else { quote!(Some(#default_expr)) }; Some(if let Some(ser_with) = field.serde_attrs.serialize_with() { quote! { { struct _SchemarsDefaultSerialize(T); impl serde::Serialize for _SchemarsDefaultSerialize<#ty> { fn serialize(&self, serializer: S) -> ::core::result::Result where S: serde::Serializer { #ser_with(&self.0, serializer) } } #default_expr.map(|d| _SchemarsDefaultSerialize(d)) } } } else { default_expr }) }