Repository: GREsau/schemars
Pull request #330 (2024-08-27 14:21:19)

Rewrite attribute handling code
(Yikes, I really dropped the ball on those first few commit messages 😬)

This was intended to be "just" a change to split the single `Attrs` type out into separate types for field/variant/containers, but it soon spiralled into a major rewrite that touched pretty much all parts of schemars_derive.

Attributes that were previously silently ignored (e.g. `with`/`schema_with` or validation attributes on structs) will now cause compile errors. Whether this is actually a breaking change is a matter of perspective, but there are definitely some other breaking changes in this PR, mostly around validation attributes:
- Remove `phone` and `required_nested` validation attributes (they were removed from the validator crate a while ago)
- `regex` and `contains` validation attributes must now be specified in list form `regex(path = ...)` rather than name/value form `regex = ...` (again for consistency with the current state of the validator crate). I don't like how verbose it makes them, but it will be slightly simpler once I've added support for garde-style attributes (in a future change)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e359a4e..cd9c0b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@
 
 - Allow `regex(path = ...)` value to be a non-string expression (https://github.com/GREsau/schemars/issues/302 / https://github.com/GREsau/schemars/pull/328)
 
+### Changed (_⚠️ possibly-breaking changes ⚠️_)
+
+- Invalid attributes that were previously silently ignored (e.g. setting `schema_with` on structs) will now cause compile errors
+- Validation attribute parsing has been altered to match the latest version of the validator crate:
+  - Remove the `phone` attribute
+  - Remove the `required_nested` attribute
+  - `regex` and `contains` attributes must now be specified in list form `#[validate(regex(path = ...))]` rather than name/value form `#[validate(regex = ...)]`
+
 ## [1.0.0-alpha.11] - 2024-08-24
 
 ### Changed
diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md
index 7449d28..bd82a9a 100644
--- a/docs/_includes/attributes.md
+++ b/docs/_includes/attributes.md
@@ -23,12 +23,12 @@ TABLE OF CONTENTS
    - [`with`](#with)
    - [`bound`](#bound)
 1. [Supported Validator Attributes](#supported-validator-attributes)
-   - [`email` / `phone` / `url`](#email-phone-url)
+   - [`email` / `url`](#email-url)
    - [`length`](#length)
    - [`range`](#range)
    - [`regex`](#regex)
    - [`contains`](#contains)
-   - [`required` / `required_nested`](#required)
+   - [`required`](#required)
 1. [Other Attributes](#other-attributes)
    - [`schema_with`](#schema_with)
    - [`title` / `description`](#title-description)
@@ -177,17 +177,16 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
 
 <div class="indented">
 
-<h3 id="email-phone-url">
+<h3 id="email-url">
 
 `#[validate(email)]` / `#[schemars(email)]`<br />
-`#[validate(phone)]` / `#[schemars(phone)]`<br />
 `#[validate(url)]` / `#[schemars(url)]`
 
 </h3>
 
-Sets the schema's `format` to `email`/`phone`/`uri`, as appropriate. Only one of these attributes may be present on a single field.
+Sets the schema's `format` to `email`/`uri`, as appropriate. Only one of these attributes may be present on a single field.
 
-Validator docs: [email](https://github.com/Keats/validator#email) / [phone](https://github.com/Keats/validator#phone) / [url](https://github.com/Keats/validator#url)
+Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)
 
 <h3 id="length">
 
@@ -212,20 +211,20 @@ Validator docs: [range](https://github.com/Keats/validator#range)
 
 <h3 id="regex">
 
-`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`<br />
-`#[schemars(regex(pattern = r"^\d+$"))]`
+`#[validate(regex(path = *static_regex)]`<br />
+`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
 
 </h3>
 
-Sets the `pattern` property for string schemas. The `path::to::regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method.
+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.
 
-Providing an inline regex pattern using `regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string.
+`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path` form is not allowed in a `#[schemars(...)]` attribute.
 
 Validator docs: [regex](https://github.com/Keats/validator#regex)
 
 <h3 id="contains">
 
-`#[validate(contains = "string")]` / `#[schemars(contains = "string")]`
+`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
 
 </h3>
 
@@ -236,13 +235,12 @@ Validator docs: [contains](https://github.com/Keats/validator#contains)
 <h3 id="required">
 
 `#[validate(required)]` / `#[schemars(required)]`<br />
-`#[validate(required_nested)]`
 
 </h3>
 
 When set on an `Option<T>` field, this will create a schemas as though the field were a `T`.
 
-Validator docs: [required](https://github.com/Keats/validator#required) / [required_nested](https://github.com/Keats/validator#required_nested)
+Validator docs: [required](https://github.com/Keats/validator#required)
 
 </div>
 
diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs
index dc9e495..30418b4 100644
--- a/docs/_includes/examples/schemars_attrs.rs
+++ b/docs/_includes/examples/schemars_attrs.rs
@@ -17,7 +17,7 @@ pub struct MyStruct {
 #[derive(Deserialize, Serialize, JsonSchema)]
 #[schemars(untagged)]
 pub enum MyEnum {
-    StringNewType(#[schemars(phone)] String),
+    StringNewType(#[schemars(email)] String),
     StructVariant {
         #[schemars(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json
index 85efffd..19a613c 100644
--- a/docs/_includes/examples/schemars_attrs.schema.json
+++ b/docs/_includes/examples/schemars_attrs.schema.json
@@ -42,7 +42,7 @@
       "anyOf": [
         {
           "type": "string",
-          "format": "phone"
+          "format": "email"
         },
         {
           "type": "object",
diff --git a/docs/_includes/examples/validate.rs b/docs/_includes/examples/validate.rs
index 4116976..70fa4a2 100644
--- a/docs/_includes/examples/validate.rs
+++ b/docs/_includes/examples/validate.rs
@@ -11,7 +11,7 @@ pub struct MyStruct {
 
 #[derive(JsonSchema)]
 pub enum MyEnum {
-    StringNewType(#[validate(phone)] String),
+    StringNewType(#[validate(email)] String),
     StructVariant {
         #[validate(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/docs/_includes/examples/validate.schema.json b/docs/_includes/examples/validate.schema.json
index e9f8a1d..c7728b8 100644
--- a/docs/_includes/examples/validate.schema.json
+++ b/docs/_includes/examples/validate.schema.json
@@ -19,7 +19,7 @@
           "properties": {
             "StringNewType": {
               "type": "string",
-              "format": "phone"
+              "format": "email"
             }
           },
           "additionalProperties": false,
diff --git a/docs/_includes/examples_v0/schemars_attrs.rs b/docs/_includes/examples_v0/schemars_attrs.rs
index 4ad2503..c8c1412 100644
--- a/docs/_includes/examples_v0/schemars_attrs.rs
+++ b/docs/_includes/examples_v0/schemars_attrs.rs
@@ -17,7 +17,7 @@ pub struct MyStruct {
 #[derive(Deserialize, Serialize, JsonSchema)]
 #[schemars(untagged)]
 pub enum MyEnum {
-    StringNewType(#[schemars(phone)] String),
+    StringNewType(#[schemars(email)] String),
     StructVariant {
         #[schemars(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/docs/_includes/examples_v0/validate.rs b/docs/_includes/examples_v0/validate.rs
index 4116976..70fa4a2 100644
--- a/docs/_includes/examples_v0/validate.rs
+++ b/docs/_includes/examples_v0/validate.rs
@@ -11,7 +11,7 @@ pub struct MyStruct {
 
 #[derive(JsonSchema)]
 pub enum MyEnum {
-    StringNewType(#[validate(phone)] String),
+    StringNewType(#[validate(email)] String),
     StructVariant {
         #[validate(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/docs/_v0/1.1-attributes.md b/docs/_v0/1.1-attributes.md
index b668d7f..ea877bb 100644
--- a/docs/_v0/1.1-attributes.md
+++ b/docs/_v0/1.1-attributes.md
@@ -191,7 +191,7 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
 <h3 id="email-phone-url">
 
 `#[validate(email)]` / `#[schemars(email)]`<br />
-`#[validate(phone)]` / `#[schemars(phone)]`<br />
+`#[validate(email)]` / `#[schemars(email)]`<br />
 `#[validate(url)]` / `#[schemars(url)]`
 
 </h3>
diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs
index dc9e495..30418b4 100644
--- a/schemars/examples/schemars_attrs.rs
+++ b/schemars/examples/schemars_attrs.rs
@@ -17,7 +17,7 @@ pub struct MyStruct {
 #[derive(Deserialize, Serialize, JsonSchema)]
 #[schemars(untagged)]
 pub enum MyEnum {
-    StringNewType(#[schemars(phone)] String),
+    StringNewType(#[schemars(email)] String),
     StructVariant {
         #[schemars(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json
index 85efffd..19a613c 100644
--- a/schemars/examples/schemars_attrs.schema.json
+++ b/schemars/examples/schemars_attrs.schema.json
@@ -42,7 +42,7 @@
       "anyOf": [
         {
           "type": "string",
-          "format": "phone"
+          "format": "email"
         },
         {
           "type": "object",
diff --git a/schemars/examples/validate.rs b/schemars/examples/validate.rs
index 4116976..70fa4a2 100644
--- a/schemars/examples/validate.rs
+++ b/schemars/examples/validate.rs
@@ -11,7 +11,7 @@ pub struct MyStruct {
 
 #[derive(JsonSchema)]
 pub enum MyEnum {
-    StringNewType(#[validate(phone)] String),
+    StringNewType(#[validate(email)] String),
     StructVariant {
         #[validate(length(min = 1, max = 100))]
         floats: Vec<f32>,
diff --git a/schemars/examples/validate.schema.json b/schemars/examples/validate.schema.json
index e9f8a1d..c7728b8 100644
--- a/schemars/examples/validate.schema.json
+++ b/schemars/examples/validate.schema.json
@@ -19,7 +19,7 @@
           "properties": {
             "StringNewType": {
               "type": "string",
-              "format": "phone"
+              "format": "email"
             }
           },
           "additionalProperties": false,
diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs
index ccae4e3..75966b8 100644
--- a/schemars/src/_private/mod.rs
+++ b/schemars/src/_private/mod.rs
@@ -4,6 +4,7 @@ use crate::{JsonSchema, Schema, SchemaGenerator};
 use serde::Serialize;
 use serde_json::{json, map::Entry, Map, Value};
 
+mod regex_syntax;
 mod rustdoc;
 
 pub use rustdoc::get_title_and_description;
@@ -141,24 +142,35 @@ pub fn insert_object_property<T: ?Sized + JsonSchema>(
     required: bool,
     sub_schema: Schema,
 ) {
-    let obj = schema.ensure_object();
-    if let Some(properties) = obj
-        .entry("properties")
-        .or_insert(Value::Object(Map::new()))
-        .as_object_mut()
-    {
-        properties.insert(key.to_owned(), sub_schema.into());
-    }
-
-    if !has_default && (required || !T::_schemars_private_is_option()) {
-        if let Some(req) = obj
-            .entry("required")
-            .or_insert(Value::Array(Vec::new()))
-            .as_array_mut()
+    fn insert_object_property_impl(
+        schema: &mut Schema,
+        key: &str,
+        has_default: bool,
+        required: bool,
+        sub_schema: Schema,
+    ) {
+        let obj = schema.ensure_object();
+        if let Some(properties) = obj
+            .entry("properties")
+            .or_insert(Value::Object(Map::new()))
+            .as_object_mut()
         {
-            req.push(key.into());
+            properties.insert(key.to_owned(), sub_schema.into());
+        }
+
+        if !has_default && (required) {
+            if let Some(req) = obj
+                .entry("required")
+                .or_insert(Value::Array(Vec::new()))
+                .as_array_mut()
+            {
+                req.push(key.into());
+            }
         }
     }
+
+    let required = required || !T::_schemars_private_is_option();
+    insert_object_property_impl(schema, key, has_default, required, sub_schema);
 }
 
 pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into<Value>) {
@@ -187,14 +199,21 @@ pub fn insert_validation_property(
     }
 }
 
-pub fn append_required(schema: &mut Schema, key: &str) {
+pub fn must_contain(schema: &mut Schema, contain: String) {
+    if schema.has_type("string") {
+        let pattern = regex_syntax::escape(&contain);
+        schema
+            .ensure_object()
+            .insert("pattern".to_owned(), pattern.into());
+    }
+
     if schema.has_type("object") {
         if let Value::Array(array) = schema
             .ensure_object()
             .entry("required")
             .or_insert(Value::Array(Vec::new()))
         {
-            let value = Value::from(key);
+            let value = Value::from(contain);
             if !array.contains(&value) {
                 array.push(value);
             }
diff --git a/schemars_derive/src/regex_syntax.rs b/schemars/src/_private/regex_syntax.rs
similarity index 98%
rename from schemars_derive/src/regex_syntax.rs
rename to schemars/src/_private/regex_syntax.rs
index 11cd638..23b67ca 100644
--- a/schemars_derive/src/regex_syntax.rs
+++ b/schemars/src/_private/regex_syntax.rs
@@ -1,4 +1,5 @@
 #![allow(clippy::all)]
+use crate::_alloc_prelude::*;
 // Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function
 // https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs
 //
diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs
index 8406712..d9ea066 100644
--- a/schemars_derive/src/ast/from_serde.rs
+++ b/schemars_derive/src/ast/from_serde.rs
@@ -1,5 +1,4 @@
 use super::*;
-use crate::attr::Attrs;
 use serde_derive_internals::ast as serde_ast;
 use serde_derive_internals::Ctxt;
 
@@ -26,8 +25,7 @@ impl<'a> FromSerde for Container<'a> {
             serde_attrs: serde.attrs,
             data: Data::from_serde(errors, serde.data)?,
             generics: serde.generics.clone(),
-            // FIXME this allows with/schema_with attribute on containers
-            attrs: Attrs::new(&serde.original.attrs, errors),
+            attrs: ContainerAttrs::new(&serde.original.attrs, errors),
         })
     }
 }
@@ -57,7 +55,7 @@ impl<'a> FromSerde for Variant<'a> {
             style: serde.style,
             fields: Field::vec_from_serde(errors, serde.fields)?,
             original: serde.original,
-            attrs: Attrs::new(&serde.original.attrs, errors),
+            attrs: VariantAttrs::new(&serde.original.attrs, errors),
         })
     }
 }
@@ -71,8 +69,7 @@ impl<'a> FromSerde for Field<'a> {
             serde_attrs: serde.attrs,
             ty: serde.ty,
             original: serde.original,
-            attrs: Attrs::new(&serde.original.attrs, errors),
-            validation_attrs: ValidationAttrs::new(&serde.original.attrs, errors),
+            attrs: FieldAttrs::new(&serde.original.attrs, errors),
         })
     }
 }
diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs
index 642721e..0b29ca1 100644
--- a/schemars_derive/src/ast/mod.rs
+++ b/schemars_derive/src/ast/mod.rs
@@ -1,7 +1,9 @@
 mod from_serde;
 
-use crate::attr::{Attrs, ValidationAttrs};
+use crate::attr::{ContainerAttrs, FieldAttrs, VariantAttrs};
+use crate::idents::SCHEMA;
 use from_serde::FromSerde;
+use proc_macro2::TokenStream;
 use serde_derive_internals::ast as serde_ast;
 use serde_derive_internals::{Ctxt, Derive};
 
@@ -10,7 +12,7 @@ pub struct Container<'a> {
     pub serde_attrs: serde_derive_internals::attr::Container,
     pub data: Data<'a>,
     pub generics: syn::Generics,
-    pub attrs: Attrs,
+    pub attrs: ContainerAttrs,
 }
 
 pub enum Data<'a> {
@@ -24,7 +26,7 @@ pub struct Variant<'a> {
     pub style: serde_ast::Style,
     pub fields: Vec<Field<'a>>,
     pub original: &'a syn::Variant,
-    pub attrs: Attrs,
+    pub attrs: VariantAttrs,
 }
 
 pub struct Field<'a> {
@@ -32,8 +34,7 @@ pub struct Field<'a> {
     pub serde_attrs: serde_derive_internals::attr::Field,
     pub ty: &'a syn::Type,
     pub original: &'a syn::Field,
-    pub attrs: Attrs,
-    pub validation_attrs: ValidationAttrs,
+    pub attrs: FieldAttrs,
 }
 
 impl<'a> Container<'a> {
@@ -60,6 +61,10 @@ impl<'a> Container<'a> {
 
         None
     }
+
+    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
+        self.attrs.common.add_mutators(mutators);
+    }
 }
 
 impl<'a> Variant<'a> {
@@ -70,10 +75,30 @@ impl<'a> Variant<'a> {
     pub fn is_unit(&self) -> bool {
         matches!(self.style, serde_ast::Style::Unit)
     }
+
+    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
+        self.attrs.common.add_mutators(mutators);
+    }
 }
 
 impl<'a> Field<'a> {
     pub fn name(&self) -> &str {
         self.serde_attrs.name().deserialize_name()
     }
+
+    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
+        self.attrs.common.add_mutators(mutators);
+        self.attrs.validation.add_mutators(mutators);
+
+        if self.serde_attrs.skip_deserializing() {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property(&mut #SCHEMA, "readOnly", true);
+            });
+        }
+        if self.serde_attrs.skip_serializing() {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property(&mut #SCHEMA, "writeOnly", true);
+            });
+        }
+    }
 }
diff --git a/schemars_derive/src/attr/doc.rs b/schemars_derive/src/attr/doc.rs
index fafd546..778d493 100644
--- a/schemars_derive/src/attr/doc.rs
+++ b/schemars_derive/src/attr/doc.rs
@@ -1,21 +1,16 @@
 use proc_macro2::TokenStream;
-use quote::ToTokens;
+use quote::TokenStreamExt;
 use syn::Attribute;
 
 pub fn get_doc(attrs: &[Attribute]) -> Option<syn::Expr> {
-    let joiner = quote! {, "\n",};
     let mut macro_args: TokenStream = TokenStream::new();
 
-    for nv in attrs
+    let lines = attrs
         .iter()
         .filter(|a| a.path().is_ident("doc"))
-        .filter_map(|a| a.meta.require_name_value().ok())
-    {
-        if !macro_args.is_empty() {
-            macro_args.extend(joiner.clone());
-        }
-        macro_args.extend(nv.value.to_token_stream());
-    }
+        .flat_map(|a| a.meta.require_name_value())
+        .map(|m| &m.value);
+    macro_args.append_separated(lines, quote!(, "\n",));
 
     if macro_args.is_empty() {
         None
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 8e7caab..9698731 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -1,371 +1,454 @@
 mod doc;
+mod parse_meta;
 mod schemars_to_serde;
 mod validation;
 
-pub use schemars_to_serde::process_serde_attrs;
-pub use validation::ValidationAttrs;
-
-use crate::metadata::SchemaMetadata;
-use proc_macro2::{Group, Span, TokenStream, TokenTree};
+use parse_meta::{parse_extensions, parse_name_value_expr, parse_name_value_lit_str};
+use proc_macro2::TokenStream;
 use quote::ToTokens;
 use serde_derive_internals::Ctxt;
-use syn::parse::{self, Parse};
-use syn::{Expr, LitStr, Meta, MetaNameValue};
+use syn::Ident;
+use syn::{punctuated::Punctuated, Attribute, Expr, ExprLit, Lit, Meta, Path, Type};
+use validation::ValidationAttrs;
+
+use crate::idents::SCHEMA;
 
-// FIXME using the same struct for containers+variants+fields means that
-//  with/schema_with are accepted (but ignored) on containers, and
-//  repr/crate_name are accepted (but ignored) on variants and fields etc.
+pub use schemars_to_serde::process_serde_attrs;
 
 #[derive(Debug, Default)]
-pub struct Attrs {
-    pub with: Option<WithAttr>,
-    pub title: Option<Expr>,
-    pub description: Option<Expr>,
+pub struct CommonAttrs {
     pub doc: Option<Expr>,
     pub deprecated: bool,
-    pub examples: Vec<syn::Path>,
-    pub repr: Option<syn::Type>,
-    pub crate_name: Option<syn::Path>,
-    pub is_renamed: bool,
+    pub title: Option<Expr>,
+    pub description: Option<Expr>,
+    pub examples: Vec<Path>,
     pub extensions: Vec<(String, TokenStream)>,
     pub transforms: Vec<Expr>,
 }
 
-#[derive(Debug)]
-pub enum WithAttr {
-    Type(syn::Type),
-    Function(syn::Path),
+#[derive(Debug, Default)]
+pub struct FieldAttrs {
+    pub common: CommonAttrs,
+    pub with: Option<WithAttr>,
+    pub validation: ValidationAttrs,
 }
 
-impl Attrs {
-    pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
-        let mut result = Attrs::default()
-            .populate(attrs, "schemars", false, errors)
-            .populate(attrs, "serde", true, errors);
-
-        result.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated"));
-        result.repr = attrs
-            .iter()
-            .find(|a| a.path().is_ident("repr"))
-            .and_then(|a| a.parse_args().ok());
+#[derive(Debug, Default)]
+pub struct ContainerAttrs {
+    pub common: CommonAttrs,
+    pub repr: Option<Type>,
+    pub crate_name: Option<Path>,
+    pub is_renamed: bool,
+}
 
-        result.doc = doc::get_doc(attrs);
-        result
-    }
+#[derive(Debug, Default)]
+pub struct VariantAttrs {
+    pub common: CommonAttrs,
+    pub with: Option<WithAttr>,
+}
 
-    pub fn as_metadata(&self) -> SchemaMetadata<'_> {
-        SchemaMetadata {
-            doc: self.doc.as_ref(),
-            title: self.title.as_ref(),
-            description: self.description.as_ref(),
-            deprecated: self.deprecated,
-            examples: &self.examples,
-            extensions: &self.extensions,
-            transforms: &self.transforms,
-            read_only: false,
-            write_only: false,
-            default: None,
-        }
-    }
+#[derive(Debug)]
+pub enum WithAttr {
+    Type(Type),
+    Function(Path),
+}
 
+impl CommonAttrs {
     fn populate(
-        mut self,
-        attrs: &[syn::Attribute],
-        attr_type: &'static str,
-        ignore_errors: bool,
-        errors: &Ctxt,
-    ) -> Self {
-        let duplicate_error = |meta: &MetaNameValue| {
-            if !ignore_errors {
-                let msg = format!(
-                    "duplicate schemars attribute `{}`",
-                    meta.path.get_ident().unwrap()
-                );
-                errors.error_spanned_by(meta, msg)
-            }
-        };
-        let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| {
-            if !ignore_errors {
-                let msg = format!(
-                    "schemars attribute cannot contain both `{}` and `{}`",
-                    meta.path.get_ident().unwrap(),
-                    other,
-                );
-                errors.error_spanned_by(meta, msg)
-            }
-        };
-
-        for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) {
-            match &meta_item {
-                Meta::NameValue(m) if m.path.is_ident("with") => {
-                    if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.value) {
-                        match self.with {
-                            Some(WithAttr::Type(_)) => duplicate_error(m),
-                            Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"),
-                            None => self.with = Some(WithAttr::Type(ty)),
-                        }
-                    }
-                }
-
-                Meta::NameValue(m) if m.path.is_ident("schema_with") => {
-                    if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.value)
-                    {
-                        match self.with {
-                            Some(WithAttr::Function(_)) => duplicate_error(m),
-                            Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"),
-                            None => self.with = Some(WithAttr::Function(fun)),
-                        }
-                    }
-                }
+        &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"));
+    }
 
-                Meta::NameValue(m) if m.path.is_ident("title") => match self.title {
-                    Some(_) => duplicate_error(m),
-                    None => self.title = Some(m.value.clone()),
-                },
+    fn process_attr(&mut self, cx: &mut AttrCtxt) {
+        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
+    }
 
-                Meta::NameValue(m) if m.path.is_ident("description") => match self.description {
-                    Some(_) => duplicate_error(m),
-                    None => self.description = Some(m.value.clone()),
-                },
+    fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
+        match meta_name {
+            "title" => match self.title {
+                Some(_) => cx.duplicate_error(&meta),
+                None => self.title = parse_name_value_expr(meta, cx).ok(),
+            },
 
-                Meta::NameValue(m) if m.path.is_ident("example") => {
-                    if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) {
-                        self.examples.push(fun)
-                    }
-                }
+            "description" => match self.description {
+                Some(_) => cx.duplicate_error(&meta),
+                None => self.description = parse_name_value_expr(meta, cx).ok(),
+            },
 
-                Meta::NameValue(m) if m.path.is_ident("rename") => self.is_renamed = true,
+            "example" => {
+                self.examples.extend(parse_name_value_lit_str(meta, cx));
+            }
 
-                Meta::NameValue(m) if m.path.is_ident("crate") && attr_type == "schemars" => {
-                    if let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.value) {
-                        if self.crate_name.is_some() {
-                            duplicate_error(m)
-                        } else {
-                            self.crate_name = Some(p)
-                        }
+            "extend" => {
+                for ex in parse_extensions(meta, cx).into_iter().flatten() {
+                    // This is O(n^2) but should be fine with the typically small number of extensions.
+                    // If this does become a problem, it can be changed to use IndexMap, or a separate Map with cloned keys.
+                    if self.extensions.iter().any(|e| e.0 == ex.key_str) {
+                        cx.error_spanned_by(
+                            ex.key_lit,
+                            format_args!("Duplicate extension key '{}'", ex.key_str),
+                        );
+                    } else {
+                        self.extensions.push((ex.key_str, ex.value));
                     }
                 }
+            }
 
-                Meta::NameValue(m) if m.path.is_ident("transform") && attr_type == "schemars" => {
-                    if let syn::Expr::Lit(syn::ExprLit {
-                        lit: syn::Lit::Str(lit_str),
+            "transform" => {
+                if let Ok(expr) = parse_name_value_expr(meta, cx) {
+                    if let Expr::Lit(ExprLit {
+                        lit: Lit::Str(lit_str),
                         ..
-                    }) = &m.value
+                    }) = &expr
                     {
-                        if parse_lit_str::<syn::Expr>(lit_str).is_ok() {
-                            errors.error_spanned_by(
-                                &m.value,
-                                format!(
+                        if lit_str.parse::<Expr>().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()
                                 ),
                             )
                         }
-                    }
-                    self.transforms.push(m.value.clone());
-                }
-
-                Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => {
-                    let parser =
-                        syn::punctuated::Punctuated::<Extension, Token![,]>::parse_terminated;
-                    match m.parse_args_with(parser) {
-                        Ok(extensions) => {
-                            for extension in extensions {
-                                let key = extension.key.value();
-                                // 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 == key) {
-                                    errors.error_spanned_by(
-                                        extension.key,
-                                        format!("Duplicate extension key '{}'", key),
-                                    );
-                                } else {
-                                    self.extensions.push((key, extension.value));
-                                }
-                            }
-                        }
-                        Err(err) => errors.syn_error(err),
-                    }
-                }
-
-                _ if ignore_errors => {}
-
-                _ => {
-                    if !is_known_serde_or_validation_keyword(&meta_item) {
-                        let path = meta_item
-                            .path()
-                            .into_token_stream()
-                            .to_string()
-                            .replace(' ', "");
-                        errors.error_spanned_by(
-                            meta_item.path(),
-                            format!("unknown schemars attribute `{}`", path),
-                        );
+                    } else {
+                        self.transforms.push(expr);
                     }
                 }
             }
+
+            _ => return Some(meta),
         }
-        self
+
+        None
     }
 
-    pub fn is_default(&self) -> bool {
-        matches!(self, Self {
-                with: None,
+    fn is_default(&self) -> bool {
+        matches!(
+            self,
+            Self {
                 title: None,
                 description: None,
                 doc: None,
                 deprecated: false,
                 examples,
-                repr: None,
-                crate_name: None,
-                is_renamed: _,
                 extensions,
-                transforms
-            } if examples.is_empty() && extensions.is_empty() && transforms.is_empty())
+                transforms,
+            } if examples.is_empty() && extensions.is_empty() && transforms.is_empty()
+        )
     }
-}
 
-fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
-    let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS
-        .iter()
-        .chain(validation::VALIDATION_KEYWORDS);
-    meta.path()
-        .get_ident()
-        .map(|i| known_keywords.any(|k| i == k))
-        .unwrap_or(false)
-}
+    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
+        let mut title = self.title.as_ref().map(ToTokens::to_token_stream);
+        let mut description = self.description.as_ref().map(ToTokens::to_token_stream);
+        if let Some(doc) = &self.doc {
+            if title.is_none() || description.is_none() {
+                mutators.push(quote!{
+                    const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc);
+                });
+                title.get_or_insert_with(|| quote!(title_and_description.0));
+                description.get_or_insert_with(|| quote!(title_and_description.1));
+            }
+        }
+        if let Some(title) = title {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "title", #title);
+            });
+        }
+        if let Some(description) = description {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "description", #description);
+            });
+        }
 
-fn get_meta_items(
-    attrs: &[syn::Attribute],
-    attr_type: &'static str,
-    errors: &Ctxt,
-    ignore_errors: bool,
-) -> Vec<Meta> {
-    let mut result = vec![];
-    for attr in attrs.iter().filter(|a| a.path().is_ident(attr_type)) {
-        match attr.parse_args_with(syn::punctuated::Punctuated::<Meta, Token![,]>::parse_terminated)
-        {
-            Ok(list) => result.extend(list),
-            Err(err) if !ignore_errors => errors.syn_error(err),
-            Err(_) => {}
+        if self.deprecated {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property(&mut #SCHEMA, "deprecated", true);
+            });
         }
-    }
 
-    result
+        if !self.examples.is_empty() {
+            let examples = self.examples.iter().map(|eg| {
+                quote! {
+                    schemars::_serde_json::value::to_value(#eg())
+                }
+            });
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property(&mut #SCHEMA, "examples", schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect()));
+            });
+        }
+
+        for (k, v) in &self.extensions {
+            mutators.push(quote! {
+                schemars::_private::insert_metadata_property(&mut #SCHEMA, #k, schemars::_serde_json::json!(#v));
+            });
+        }
+
+        for transform in &self.transforms {
+            mutators.push(quote! {
+                schemars::transform::Transform::transform(&mut #transform, &mut #SCHEMA);
+            });
+        }
+    }
 }
 
-fn expr_as_lit_str<'a>(
-    cx: &Ctxt,
-    attr_type: &'static str,
-    meta_item_name: &'static str,
-    expr: &'a syn::Expr,
-) -> Result<&'a syn::LitStr, ()> {
-    if let syn::Expr::Lit(syn::ExprLit {
-        lit: syn::Lit::Str(lit_str),
-        ..
-    }) = expr
-    {
-        Ok(lit_str)
-    } else {
-        cx.error_spanned_by(
-            expr,
-            format!(
-                "expected {} {} attribute to be a string: `{} = \"...\"`",
-                attr_type, meta_item_name, meta_item_name
-            ),
-        );
-        Err(())
+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");
+
+        self.common.populate(attrs, schemars_cx, serde_cx);
+        self.validation.populate(schemars_cx, validate_cx);
+        self.process_attr(schemars_cx);
+        self.process_attr(serde_cx);
+    }
+
+    fn process_attr(&mut self, cx: &mut AttrCtxt) {
+        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
+    }
+
+    fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
+        match meta_name {
+            "with" => match self.with {
+                Some(WithAttr::Type(_)) => cx.duplicate_error(&meta),
+                Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"),
+                None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type),
+            },
+            "schema_with" if cx.attr_type == "schemars" => match self.with {
+                Some(WithAttr::Function(_)) => cx.duplicate_error(&meta),
+                Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"),
+                None => {
+                    self.with = parse_name_value_lit_str(meta, cx)
+                        .ok()
+                        .map(WithAttr::Function)
+                }
+            },
+
+            _ => return Some(meta),
+        }
+
+        None
     }
 }
 
-fn parse_lit_into_ty(
-    cx: &Ctxt,
-    attr_type: &'static str,
-    meta_item_name: &'static str,
-    lit: &syn::Expr,
-) -> Result<syn::Type, ()> {
-    let string = expr_as_lit_str(cx, attr_type, meta_item_name, lit)?;
-
-    parse_lit_str(string).map_err(|_| {
-        cx.error_spanned_by(
-            lit,
-            format!(
-                "failed to parse type: `{} = {:?}`",
-                meta_item_name,
-                string.value()
-            ),
-        )
-    })
+impl ContainerAttrs {
+    pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self {
+        let mut result = Self::default();
+        result.populate(attrs, cx);
+        result
+    }
+
+    fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) {
+        let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars");
+        let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde");
+
+        self.common.populate(attrs, schemars_cx, serde_cx);
+        self.process_attr(schemars_cx);
+        self.process_attr(serde_cx);
+
+        self.repr = attrs
+            .iter()
+            .find(|a| a.path().is_ident("repr"))
+            .and_then(|a| a.parse_args().ok());
+    }
+
+    fn process_attr(&mut self, cx: &mut AttrCtxt) {
+        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
+    }
+
+    fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
+        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" => self.is_renamed = true,
+
+            _ => return Some(meta),
+        };
+
+        None
+    }
 }
 
-fn parse_lit_into_path(
-    cx: &Ctxt,
-    attr_type: &'static str,
-    meta_item_name: &'static str,
-    expr: &syn::Expr,
-) -> Result<syn::Path, ()> {
-    let lit_str = expr_as_lit_str(cx, attr_type, meta_item_name, expr)?;
-
-    parse_lit_str(lit_str).map_err(|_| {
-        cx.error_spanned_by(
-            expr,
-            format!(
-                "failed to parse path: `{} = {:?}`",
-                meta_item_name,
-                lit_str.value()
-            ),
+impl VariantAttrs {
+    pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self {
+        let mut result = Self::default();
+        result.populate(attrs, cx);
+        result
+    }
+
+    fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) {
+        let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars");
+        let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde");
+
+        self.common.populate(attrs, schemars_cx, serde_cx);
+        self.process_attr(schemars_cx);
+        self.process_attr(serde_cx);
+    }
+
+    fn process_attr(&mut self, cx: &mut AttrCtxt) {
+        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
+    }
+
+    fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
+        match meta_name {
+            "with" => match self.with {
+                Some(WithAttr::Type(_)) => cx.duplicate_error(&meta),
+                Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"),
+                None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type),
+            },
+            "schema_with" if cx.attr_type == "schemars" => match self.with {
+                Some(WithAttr::Function(_)) => cx.duplicate_error(&meta),
+                Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"),
+                None => {
+                    self.with = parse_name_value_lit_str(meta, cx)
+                        .ok()
+                        .map(WithAttr::Function)
+                }
+            },
+
+            _ => return Some(meta),
+        }
+
+        None
+    }
+
+    pub fn is_default(&self) -> bool {
+        matches!(
+            self,
+            Self {
+                common,
+                with: None,
+            } if common.is_default()
         )
-    })
+    }
 }
 
-fn parse_lit_str<T>(s: &syn::LitStr) -> parse::Result<T>
-where
-    T: Parse,
-{
-    let tokens = spanned_tokens(s)?;
-    syn::parse2(tokens)
+fn get_meta_items(attrs: &[Attribute], attr_type: &'static str, cx: &Ctxt) -> Vec<Meta> {
+    let mut result = vec![];
+
+    for attr in attrs.iter().filter(|a| a.path().is_ident(attr_type)) {
+        match attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated) {
+            Ok(list) => result.extend(list),
+            Err(err) => {
+                if attr_type == "schemars" {
+                    cx.syn_error(err)
+                }
+            }
+        }
+    }
+
+    result
 }
 
-fn spanned_tokens(s: &syn::LitStr) -> parse::Result<TokenStream> {
-    let stream = syn::parse_str(&s.value())?;
-    Ok(respan_token_stream(stream, s.span()))
+fn path_str(path: &Path) -> String {
+    path.get_ident()
+        .map(Ident::to_string)
+        .unwrap_or_else(|| path.into_token_stream().to_string().replace(' ', ""))
 }
 
-fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream {
-    stream
-        .into_iter()
-        .map(|token| respan_token_tree(token, span))
-        .collect()
+pub struct AttrCtxt<'a> {
+    inner: &'a Ctxt,
+    attr_type: &'static str,
+    metas: Vec<Meta>,
 }
 
-fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree {
-    if let TokenTree::Group(g) = &mut token {
-        *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span));
+impl<'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),
+        }
     }
-    token.set_span(span);
-    token
-}
 
-#[derive(Debug)]
-struct Extension {
-    key: LitStr,
-    value: TokenStream,
-}
+    pub fn new_nested_meta(&self, metas: Vec<Meta>) -> Self {
+        Self { metas, ..*self }
+    }
 
-impl Parse for Extension {
-    fn parse(input: parse::ParseStream) -> syn::Result<Self> {
-        let key = input.parse::<LitStr>()?;
-        input.parse::<Token![=]>()?;
-        let mut value = TokenStream::new();
+    pub fn parse_meta(&mut self, mut handle: impl FnMut(Meta, &str, &Self) -> Option<Meta>) {
+        let metas = std::mem::take(&mut self.metas);
+        self.metas = metas
+            .into_iter()
+            .filter_map(|meta| {
+                meta.path()
+                    .get_ident()
+                    .map(Ident::to_string)
+                    .and_then(|name| handle(meta, &name, self))
+            })
+            .collect();
+    }
 
-        while !input.is_empty() && !input.peek(Token![,]) {
-            value.extend([input.parse::<TokenTree>()?]);
+    pub fn error_spanned_by<A: ToTokens, T: std::fmt::Display>(&self, obj: A, msg: T) {
+        self.inner.error_spanned_by(obj, msg);
+    }
+
+    pub fn syn_error(&self, err: syn::Error) {
+        self.inner.syn_error(err);
+    }
+
+    pub fn mutual_exclusive_error(&self, meta: &Meta, other_attr: &str) {
+        if self.attr_type == "schemars" {
+            self.error_spanned_by(
+                meta,
+                format_args!(
+                    "schemars attribute cannot contain both `{}` and `{}`",
+                    path_str(meta.path()),
+                    other_attr,
+                ),
+            );
         }
+    }
 
-        if value.is_empty() {
-            return Err(syn::Error::new(input.span(), "Expected extension value"));
+    pub fn duplicate_error(&self, meta: &Meta) {
+        if self.attr_type == "schemars" {
+            self.error_spanned_by(
+                meta,
+                format_args!(
+                    "duplicate schemars attribute item `{}`",
+                    path_str(meta.path())
+                ),
+            );
         }
+    }
+}
 
-        Ok(Extension { key, value })
+impl Drop for AttrCtxt<'_> {
+    fn drop(&mut self) {
+        if self.attr_type == "schemars" {
+            for unhandled_meta in self.metas.iter().filter(|m| !is_known_serde_keyword(m)) {
+                self.error_spanned_by(
+                    unhandled_meta.path(),
+                    format_args!(
+                        "unknown schemars attribute `{}`",
+                        path_str(unhandled_meta.path())
+                    ),
+                );
+            }
+        }
     }
 }
+
+fn is_known_serde_keyword(meta: &Meta) -> bool {
+    let known_keywords = schemars_to_serde::SERDE_KEYWORDS;
+    meta.path()
+        .get_ident()
+        .map(|i| known_keywords.contains(&i.to_string().as_str()))
+        .unwrap_or(false)
+}
diff --git a/schemars_derive/src/attr/parse_meta.rs b/schemars_derive/src/attr/parse_meta.rs
new file mode 100644
index 0000000..f77b732
--- /dev/null
+++ b/schemars_derive/src/attr/parse_meta.rs
@@ -0,0 +1,285 @@
+use proc_macro2::{TokenStream, TokenTree};
+use syn::{
+    parse::{Parse, ParseStream, Parser},
+    punctuated::Punctuated,
+    Expr, ExprLit, Lit, LitStr, Meta, MetaNameValue,
+};
+
+use super::{path_str, AttrCtxt};
+
+pub fn require_path_only(meta: Meta, cx: &AttrCtxt) -> Result<(), ()> {
+    match meta {
+        Meta::Path(_) => Ok(()),
+        _ => {
+            let name = path_str(meta.path());
+            cx.error_spanned_by(
+                meta,
+                format_args!(
+                    "unexpected value of {} {} attribute item",
+                    cx.attr_type, name
+                ),
+            );
+            Err(())
+        }
+    }
+}
+
+pub fn parse_name_value_expr(meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    match meta {
+        Meta::NameValue(m) => Ok(m.value),
+        _ => {
+            let name = path_str(meta.path());
+            cx.error_spanned_by(
+                meta,
+                format_args!(
+                    "expected {} {} attribute item to have a value: `{} = ...`",
+                    cx.attr_type, name, name
+                ),
+            );
+            Err(())
+        }
+    }
+}
+
+pub fn parse_name_value_lit_str<T: Parse>(meta: Meta, cx: &AttrCtxt) -> Result<T, ()> {
+    let Meta::NameValue(MetaNameValue {
+        value: Expr::Lit(ExprLit {
+            lit: Lit::Str(lit_str),
+            ..
+        }),
+        ..
+    }) = meta
+    else {
+        let name = path_str(meta.path());
+        cx.error_spanned_by(
+            meta,
+            format_args!(
+                "expected {} {} attribute item to have a string value: `{} = \"...\"`",
+                cx.attr_type, name, name
+            ),
+        );
+        return Err(());
+    };
+
+    parse_lit_str(lit_str, cx)
+}
+
+fn parse_lit_str<T: Parse>(lit_str: LitStr, cx: &AttrCtxt) -> Result<T, ()> {
+    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::<T>()
+                    .rsplit("::")
+                    .next()
+                    .unwrap_or_default()
+                    .to_ascii_lowercase(),
+            ),
+        );
+    })
+}
+
+pub fn parse_extensions(
+    meta: Meta,
+    cx: &AttrCtxt,
+) -> Result<impl IntoIterator<Item = Extension>, ()> {
+    let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
+    parse_meta_list(meta, cx, parser)
+}
+
+pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
+    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_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    let mut pattern = None;
+
+    for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
+        match path_str(nested_meta.path()).as_str() {
+            "pattern" => match &pattern {
+                Some(_) => cx.duplicate_error(&nested_meta),
+                None => pattern = parse_name_value_expr(nested_meta, cx).ok(),
+            },
+            "path" => {
+                cx.error_spanned_by(nested_meta, "`path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead")
+            },
+            unknown => {
+                cx.error_spanned_by(
+                    nested_meta,
+                    format_args!("unknown item in schemars `regex` attribute: `{unknown}`"),
+                );
+            }
+        }
+    }
+
+    pattern.ok_or_else(|| {
+        cx.error_spanned_by(
+            outer_meta,
+            "`schemars(regex(...))` attribute requires `pattern = ...`",
+        )
+    })
+}
+
+pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    let mut path = None;
+
+    for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
+        match path_str(nested_meta.path()).as_str() {
+            "path" => match &path{
+                Some(_) => cx.duplicate_error(&nested_meta),
+                None => path = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(),
+            },
+            "pattern" => {
+                cx.error_spanned_by(nested_meta, "`pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead")
+            },
+            _ => {
+                // ignore unknown properties in `validate` attribute
+            }
+        }
+    }
+
+    path.ok_or_else(|| {
+        cx.error_spanned_by(
+            outer_meta,
+            "`validate(regex(...))` attribute requires `path = ...`",
+        )
+    })
+}
+
+pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    let mut pattern = None;
+
+    for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
+        match path_str(nested_meta.path()).as_str() {
+            "pattern" => match &pattern {
+                Some(_) => cx.duplicate_error(&nested_meta),
+                None => pattern = parse_name_value_expr(nested_meta, cx).ok(),
+            },
+            unknown => {
+                if cx.attr_type == "schemars" {
+                    cx.error_spanned_by(
+                        nested_meta,
+                        format_args!("unknown item in schemars `contains` attribute: `{unknown}`"),
+                    );
+                }
+            }
+        }
+    }
+
+    pattern.ok_or_else(|| {
+        cx.error_spanned_by(
+            outer_meta,
+            "`contains` attribute item requires `pattern = ...`",
+        )
+    })
+}
+
+pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result<impl IntoIterator<Item = Meta>, ()> {
+    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
+    parse_meta_list(meta, cx, parser)
+}
+
+fn parse_meta_list<F: Parser>(meta: Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
+    let Meta::List(meta_list) = meta else {
+        let name = path_str(meta.path());
+        cx.error_spanned_by(
+            meta,
+            format_args!(
+                "expected {} {} attribute item to be of the form `{}(...)`",
+                cx.attr_type, name, name
+            ),
+        );
+        return Err(());
+    };
+
+    meta_list.parse_args_with(parser).map_err(|err| {
+        cx.syn_error(err);
+    })
+}
+
+// Like `parse_name_value_expr`, but if the result is a string literal, then parse its contents.
+pub fn parse_name_value_expr_handle_lit_str(meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
+    let expr = parse_name_value_expr(meta, cx)?;
+
+    if let Expr::Lit(ExprLit {
+        lit: Lit::Str(lit_str),
+        ..
+    }) = expr
+    {
+        parse_lit_str(lit_str, cx)
+    } else {
+        Ok(expr)
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct LengthOrRange {
+    pub min: Option<Expr>,
+    pub max: Option<Expr>,
+    pub equal: Option<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Extension {
+    pub key_str: String,
+    pub key_lit: LitStr,
+    pub value: TokenStream,
+}
+
+impl Parse for Extension {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let key = input.parse::<LitStr>()?;
+        input.parse::<Token![=]>()?;
+        let mut value = TokenStream::new();
+
+        while !input.is_empty() && !input.peek(Token![,]) {
+            value.extend([input.parse::<TokenTree>()?]);
+        }
+
+        if value.is_empty() {
+            return Err(syn::Error::new(input.span(), "Expected extension value"));
+        }
+
+        Ok(Extension {
+            key_str: key.value(),
+            key_lit: key,
+            value,
+        })
+    }
+}
diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs
index 5b69942..80bcaf0 100644
--- a/schemars_derive/src/attr/schemars_to_serde.rs
+++ b/schemars_derive/src/attr/schemars_to_serde.rs
@@ -67,7 +67,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
 
     // Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes
     let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) =
-        get_meta_items(attrs, "schemars", ctxt, false)
+        get_meta_items(attrs, "schemars", ctxt)
             .into_iter()
             .filter_map(|meta| {
                 let keyword = get_meta_ident(&meta)?;
@@ -85,7 +85,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
     }
 
     // Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes
-    for meta in get_meta_items(&serde_attrs, "serde", ctxt, false) {
+    for meta in get_meta_items(&serde_attrs, "serde", ctxt) {
         if let Some(i) = get_meta_ident(&meta) {
             if !schemars_meta_names.contains(&i)
                 && SERDE_KEYWORDS.contains(&i.as_ref())
diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs
index 0d9acf9..ec26308 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -1,21 +1,20 @@
-use super::{expr_as_lit_str, get_meta_items, parse_lit_str};
 use proc_macro2::TokenStream;
-use quote::ToTokens;
-use serde_derive_internals::Ctxt;
-use syn::{
-    parse::Parser, punctuated::Punctuated, Expr, ExprLit, ExprPath, Lit, Meta, MetaList,
-    MetaNameValue, Path,
-};
+use syn::{Expr, Meta};
+
+use crate::idents::SCHEMA;
 
-pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
-    "range", "regex", "contains", "email", "phone", "url", "length", "required", "inner",
-];
+use super::{
+    parse_meta::{
+        parse_contains, parse_length_or_range, parse_nested_meta, parse_schemars_regex,
+        parse_validate_regex, require_path_only, LengthOrRange,
+    },
+    AttrCtxt,
+};
 
 #[derive(Debug, Clone, Copy, PartialEq)]
-enum Format {
+pub enum Format {
     Email,
     Uri,
-    Phone,
 }
 
 impl Format {
@@ -23,7 +22,6 @@ impl Format {
         match self {
             Format::Email => "email",
             Format::Uri => "url",
-            Format::Phone => "phone",
         }
     }
 
@@ -31,420 +29,167 @@ impl Format {
         match self {
             Format::Email => "email",
             Format::Uri => "uri",
-            Format::Phone => "phone",
+        }
+    }
+
+    fn from_attr_str(s: &str) -> Self {
+        match s {
+            "email" => Format::Email,
+            "url" => Format::Uri,
+            _ => panic!("Invalid format attr string `{s}`. This is a bug in schemars, please raise an issue!"),
         }
     }
 }
 
 #[derive(Debug, Default)]
 pub struct ValidationAttrs {
-    length_min: Option<Expr>,
-    length_max: Option<Expr>,
-    length_equal: Option<Expr>,
-    range_min: Option<Expr>,
-    range_max: Option<Expr>,
-    regex: Option<Expr>,
-    contains: Option<String>,
-    required: bool,
-    format: Option<Format>,
-    inner: Option<Box<ValidationAttrs>>,
+    pub length: Option<LengthOrRange>,
+    pub range: Option<LengthOrRange>,
+    pub regex: Option<Expr>,
+    pub contains: Option<Expr>,
+    pub required: bool,
+    pub format: Option<Format>,
+    pub inner: Option<Box<ValidationAttrs>>,
 }
 
 impl ValidationAttrs {
-    pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
-        let schemars_items = get_meta_items(attrs, "schemars", errors, false);
-        let validate_items = get_meta_items(attrs, "validate", errors, true);
-
-        ValidationAttrs::default()
-            .populate(schemars_items, "schemars", false, errors)
-            .populate(validate_items, "validate", true, errors)
-    }
-
-    pub fn required(&self) -> bool {
-        self.required
+    pub fn add_mutators(&self, mutators: &mut Vec<TokenStream>) {
+        self.add_mutators2(mutators, &quote!(&mut #SCHEMA));
     }
 
-    fn populate(
-        mut self,
-        meta_items: Vec<Meta>,
-        attr_type: &'static str,
-        ignore_errors: bool,
-        errors: &Ctxt,
-    ) -> Self {
-        let duplicate_error = |path: &Path| {
-            if !ignore_errors {
-                let msg = format!(
-                    "duplicate schemars attribute `{}`",
-                    path.get_ident().unwrap()
-                );
-                errors.error_spanned_by(path, msg)
-            }
-        };
-        let mutual_exclusive_error = |path: &Path, other: &str| {
-            if !ignore_errors {
-                let msg = format!(
-                    "schemars attribute cannot contain both `{}` and `{}`",
-                    path.get_ident().unwrap(),
-                    other,
-                );
-                errors.error_spanned_by(path, msg)
-            }
-        };
-        let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| {
-            if !ignore_errors {
-                let msg = if existing == new {
-                    format!("duplicate schemars attribute `{}`", existing.attr_str())
-                } else {
-                    format!(
-                        "schemars attribute cannot contain both `{}` and `{}`",
-                        existing.attr_str(),
-                        new.attr_str(),
-                    )
-                };
-                errors.error_spanned_by(path, msg)
-            }
-        };
-        let parse_nested_meta = |meta_list: MetaList| {
-            let parser = Punctuated::<syn::Meta, Token![,]>::parse_terminated;
-            match parser.parse2(meta_list.tokens) {
-                Ok(p) => p,
-                Err(e) => {
-                    if !ignore_errors {
-                        errors.syn_error(e);
-                    }
-                    Default::default()
-                }
-            }
-        };
-
-        for meta_item in meta_items {
-            match meta_item {
-                Meta::List(meta_list) if meta_list.path.is_ident("length") => {
-                    for nested in parse_nested_meta(meta_list) {
-                        match nested {
-                            Meta::NameValue(nv) if nv.path.is_ident("min") => {
-                                if self.length_min.is_some() {
-                                    duplicate_error(&nv.path)
-                                } else if self.length_equal.is_some() {
-                                    mutual_exclusive_error(&nv.path, "equal")
-                                } else {
-                                    self.length_min = str_or_num_to_expr(errors, "min", nv.value);
-                                }
-                            }
-                            Meta::NameValue(nv) if nv.path.is_ident("max") => {
-                                if self.length_max.is_some() {
-                                    duplicate_error(&nv.path)
-                                } else if self.length_equal.is_some() {
-                                    mutual_exclusive_error(&nv.path, "equal")
-                                } else {
-                                    self.length_max = str_or_num_to_expr(errors, "max", nv.value);
-                                }
-                            }
-                            Meta::NameValue(nv) if nv.path.is_ident("equal") => {
-                                if self.length_equal.is_some() {
-                                    duplicate_error(&nv.path)
-                                } else if self.length_min.is_some() {
-                                    mutual_exclusive_error(&nv.path, "min")
-                                } else if self.length_max.is_some() {
-                                    mutual_exclusive_error(&nv.path, "max")
-                                } else {
-                                    self.length_equal =
-                                        str_or_num_to_expr(errors, "equal", nv.value);
-                                }
-                            }
-                            meta => {
-                                if !ignore_errors {
-                                    errors.error_spanned_by(
-                                        meta,
-                                        "unknown item in schemars length attribute",
-                                    );
-                                }
-                            }
-                        }
-                    }
-                }
-
-                Meta::List(meta_list) if meta_list.path.is_ident("range") => {
-                    for nested in parse_nested_meta(meta_list) {
-                        match nested {
-                            Meta::NameValue(nv) if nv.path.is_ident("min") => {
-                                if self.range_min.is_some() {
-                                    duplicate_error(&nv.path)
-                                } else {
-                                    self.range_min = str_or_num_to_expr(errors, "min", nv.value);
-                                }
-                            }
-                            Meta::NameValue(nv) if nv.path.is_ident("max") => {
-                                if self.range_max.is_some() {
-                                    duplicate_error(&nv.path)
-                                } else {
-                                    self.range_max = str_or_num_to_expr(errors, "max", nv.value);
-                                }
-                            }
-                            meta => {
-                                if !ignore_errors {
-                                    errors.error_spanned_by(
-                                        meta,
-                                        "unknown item in schemars range attribute",
-                                    );
-                                }
-                            }
-                        }
-                    }
-                }
-
-                Meta::Path(m) if m.is_ident("required") || m.is_ident("required_nested") => {
-                    self.required = true;
-                }
-
-                Meta::Path(p) if p.is_ident(Format::Email.attr_str()) => match self.format {
-                    Some(f) => duplicate_format_error(f, Format::Email, &p),
-                    None => self.format = Some(Format::Email),
-                },
-                Meta::Path(p) if p.is_ident(Format::Uri.attr_str()) => match self.format {
-                    Some(f) => duplicate_format_error(f, Format::Uri, &p),
-                    None => self.format = Some(Format::Uri),
-                },
-                Meta::Path(p) if p.is_ident(Format::Phone.attr_str()) => match self.format {
-                    Some(f) => duplicate_format_error(f, Format::Phone, &p),
-                    None => self.format = Some(Format::Phone),
-                },
-
-                Meta::NameValue(nv) if nv.path.is_ident("regex") => {
-                    match (&self.regex, &self.contains) {
-                        (Some(_), _) => duplicate_error(&nv.path),
-                        (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"),
-                        (None, None) => {
-                            self.regex = parse_regex_expr(errors, nv.value);
-                        }
-                    }
-                }
+    fn add_mutators2(&self, mutators: &mut Vec<TokenStream>, 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);
+        }
 
-                Meta::List(meta_list) if meta_list.path.is_ident("regex") => {
-                    match (&self.regex, &self.contains) {
-                        (Some(_), _) => duplicate_error(&meta_list.path),
-                        (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"),
-                        (None, None) => {
-                            for x in parse_nested_meta(meta_list) {
-                                match x {
-                                    Meta::NameValue(MetaNameValue { path, value, .. })
-                                        if path.is_ident("path") =>
-                                    {
-                                        self.regex = parse_regex_expr(errors, value);
-                                    }
-                                    Meta::NameValue(MetaNameValue { path, value, .. })
-                                        if path.is_ident("pattern") =>
-                                    {
-                                        self.regex =
-                                            expr_as_lit_str(errors, attr_type, "pattern", &value)
-                                                .ok()
-                                                .map(|litstr| {
-                                                    Expr::Lit(ExprLit {
-                                                        attrs: Vec::new(),
-                                                        lit: Lit::Str(litstr.clone()),
-                                                    })
-                                                })
-                                    }
-                                    meta => {
-                                        if !ignore_errors {
-                                            errors.error_spanned_by(
-                                                meta,
-                                                "unknown item in schemars regex attribute",
-                                            );
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+        if let Some(range) = &self.range {
+            Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema);
+        }
 
-                Meta::NameValue(MetaNameValue { path, value, .. }) if path.is_ident("contains") => {
-                    match (&self.contains, &self.regex) {
-                        (Some(_), _) => duplicate_error(&path),
-                        (None, Some(_)) => mutual_exclusive_error(&path, "regex"),
-                        (None, None) => {
-                            self.contains = expr_as_lit_str(errors, attr_type, "contains", &value)
-                                .map(|litstr| litstr.value())
-                                .ok()
-                        }
-                    }
-                }
+        if let Some(regex) = &self.regex {
+            mutators.push(quote! {
+                schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", #regex.to_string());
+            });
+        }
 
-                Meta::List(meta_list) if meta_list.path.is_ident("contains") => {
-                    match (&self.contains, &self.regex) {
-                        (Some(_), _) => duplicate_error(&meta_list.path),
-                        (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"),
-                        (None, None) => {
-                            for x in parse_nested_meta(meta_list) {
-                                match x {
-                                    Meta::NameValue(MetaNameValue { path, value, .. })
-                                        if path.is_ident("pattern") =>
-                                    {
-                                        self.contains =
-                                            expr_as_lit_str(errors, attr_type, "contains", &value)
-                                                .ok()
-                                                .map(|litstr| litstr.value())
-                                    }
-                                    meta => {
-                                        if !ignore_errors {
-                                            errors.error_spanned_by(
-                                                meta,
-                                                "unknown item in schemars contains attribute",
-                                            );
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+        if let Some(contains) = &self.contains {
+            mutators.push(quote! {
+                schemars::_private::must_contain(#mut_ref_schema, #contains.to_string());
+            });
+        }
 
-                Meta::List(meta_list) if meta_list.path.is_ident("inner") => match self.inner {
-                    Some(_) => duplicate_error(&meta_list.path),
-                    None => {
-                        let inner_attrs = ValidationAttrs::default().populate(
-                            parse_nested_meta(meta_list).into_iter().collect(),
-                            attr_type,
-                            ignore_errors,
-                            errors,
-                        );
-                        self.inner = Some(Box::new(inner_attrs));
-                    }
-                },
+        if let Some(format) = &self.format {
+            let f = format.schema_str();
+            mutators.push(quote! {
+                (#mut_ref_schema).ensure_object().insert("format".into(), #f.into());
+            })
+        };
 
-                _ if ignore_errors => {}
+        if let Some(inner) = &self.inner {
+            let mut inner_mutators = Vec::new();
+            inner.add_mutators2(&mut inner_mutators, &quote!(inner_schema));
 
-                _ => {
-                    if let Some(ident) = meta_item.path().get_ident() {
-                        if VALIDATION_KEYWORDS.iter().any(|k| ident == k) {
-                            errors.error_spanned_by(
-                                &meta_item,
-                                format!("could not parse `{ident}` item in schemars attribute"),
-                            );
-                        }
-                    }
-                }
+            if !inner_mutators.is_empty() {
+                mutators.push(quote! {
+                    schemars::_private::apply_inner_validation(#mut_ref_schema, |inner_schema| { #(#inner_mutators)* });
+                })
             }
         }
-        self
     }
 
-    pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
-        let setters = self.make_setters(quote!(&mut schema));
-        if !setters.is_empty() {
-            *schema_expr = quote!({
-                let mut schema = #schema_expr;
-                #(#setters)*
-                schema
+    fn add_length_or_range(
+        value: &LengthOrRange,
+        mutators: &mut Vec<TokenStream>,
+        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);
             });
         }
-    }
-
-    fn make_setters(&self, mut_schema: impl ToTokens) -> Vec<TokenStream> {
-        let mut result = Vec::new();
 
-        if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "string", "minLength", #length_min);
-            });
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "array", "minItems", #length_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);
             });
         }
+    }
 
-        if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "string", "maxLength", #length_max);
-            });
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "array", "maxItems", #length_max);
-            });
-        }
+    pub(super) fn populate(&mut self, schemars_cx: &mut AttrCtxt, validate_cx: &mut AttrCtxt) {
+        self.process_attr(schemars_cx);
+        self.process_attr(validate_cx);
+    }
 
-        if let Some(range_min) = &self.range_min {
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "number", "minimum", #range_min);
-            });
-        }
+    fn process_attr(&mut self, cx: &mut AttrCtxt) {
+        cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
+    }
 
-        if let Some(range_max) = &self.range_max {
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "number", "maximum", #range_max);
-            });
-        }
+    fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
+        match meta_name {
+            "length" => match self.length {
+                Some(_) => cx.duplicate_error(&meta),
+                None => self.length = parse_length_or_range(meta, cx).ok(),
+            },
 
-        if let Some(regex) = &self.regex {
-            result.push(quote! {
-                schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #regex);
-            });
-        }
+            "range" => match self.range {
+                Some(_) => cx.duplicate_error(&meta),
+                None => self.range = parse_length_or_range(meta, cx).ok(),
+            },
 
-        if let Some(contains) = &self.contains {
-            result.push(quote! {
-                schemars::_private::append_required(#mut_schema, #contains);
-            });
+            "email" | "url" => self.handle_format(meta, meta_name, cx),
 
-            if self.regex.is_none() {
-                let pattern = crate::regex_syntax::escape(contains);
-                result.push(quote! {
-                    schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #pattern);
-                });
+            "required" => {
+                if self.required {
+                    cx.duplicate_error(&meta);
+                } else if require_path_only(meta, cx).is_ok() {
+                    self.required = true;
+                }
             }
-        }
 
-        if let Some(format) = &self.format {
-            let f = format.schema_str();
-            result.push(quote! {
-                schema.ensure_object().insert("format".into(), #f.into());
-            })
-        };
-
-        if let Some(inner) = &self.inner {
-            let inner_setters = inner.make_setters(quote!(schema));
-            if !inner_setters.is_empty() {
-                result.push(quote! {
-                    schemars::_private::apply_inner_validation(#mut_schema, |schema| { #(#inner_setters)* });
-                })
+            "regex" => match (&self.regex, &self.contains, cx.attr_type) {
+                (Some(_), _, _) => cx.duplicate_error(&meta),
+                (_, Some(_), _) => cx.mutual_exclusive_error(&meta, "contains"),
+                (None, None, "schemars") => self.regex = parse_schemars_regex(meta, cx).ok(),
+                (None, None, "validate") => self.regex = parse_validate_regex(meta, cx).ok(),
+                (None, None, wat) => panic!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!"),
+            },
+            "contains" => match (&self.regex, &self.contains) {
+                (Some(_), _) => cx.mutual_exclusive_error(&meta, "regex"),
+                (_, Some(_)) => cx.duplicate_error(&meta),
+                (None, None) => self.contains = parse_contains(meta, cx).ok(),
+            },
+
+            "inner" => {
+                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);
+                }
             }
-        }
-
-        result
-    }
-}
 
-fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Expr> {
-    // this odd double-parsing is to make `-10` parsed as an Lit instead of an Expr::Unary
-    let lit: Lit = match syn::parse2(expr.to_token_stream()) {
-        Ok(l) => l,
-        Err(err) => {
-            cx.syn_error(err);
-            return None;
+            _ => return Some(meta),
         }
-    };
 
-    match lit {
-        Lit::Str(s) => parse_lit_str::<ExprPath>(&s).ok().map(Expr::Path),
-        Lit::Int(_) | Lit::Float(_) => Some(expr),
-        _ => {
-            cx.error_spanned_by(
-                &expr,
-                format!(
-                    "expected `{}` to be a string or number literal, not {:?}",
-                    meta_item_name, &expr
-                ),
-            );
-            None
-        }
+        None
     }
-}
 
-fn parse_regex_expr(cx: &Ctxt, value: Expr) -> Option<Expr> {
-    match value {
-        Expr::Lit(ExprLit {
-            lit: Lit::Str(litstr),
-            ..
-        }) => parse_lit_str(&litstr).map_err(|e| cx.syn_error(e)).ok(),
-        v => Some(v),
+    fn handle_format(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) {
+        match &self.format {
+            Some(f) if f.attr_str() == meta_name => cx.duplicate_error(&meta),
+            Some(f) => cx.mutual_exclusive_error(&meta, f.attr_str()),
+            None => {
+                // FIXME this is too strict - it may be a MetaList in validator attr (e.g. with message/code items)
+                if require_path_only(meta, cx).is_ok() {
+                    self.format = Some(Format::from_attr_str(meta_name))
+                }
+            }
+        }
     }
 }
diff --git a/schemars_derive/src/idents.rs b/schemars_derive/src/idents.rs
new file mode 100644
index 0000000..d940a62
--- /dev/null
+++ b/schemars_derive/src/idents.rs
@@ -0,0 +1,15 @@
+use proc_macro2::{Ident, Span, TokenStream, TokenTree};
+use quote::TokenStreamExt;
+
+pub const GENERATOR: ConstIdent = ConstIdent("generator");
+pub const SCHEMA: ConstIdent = ConstIdent("schema");
+pub const STRUCT_DEFAULT: ConstIdent = ConstIdent("struct_default");
+
+pub struct ConstIdent(&'static str);
+
+impl quote::ToTokens for ConstIdent {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let ident = Ident::new(self.0, Span::call_site());
+        tokens.append(TokenTree::from(ident));
+    }
+}
diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs
index 6954519..48afe80 100644
--- a/schemars_derive/src/lib.rs
+++ b/schemars_derive/src/lib.rs
@@ -8,11 +8,11 @@ extern crate proc_macro;
 
 mod ast;
 mod attr;
-mod metadata;
-mod regex_syntax;
+mod idents;
 mod schema_exprs;
 
 use ast::*;
+use idents::GENERATOR;
 use proc_macro2::TokenStream;
 use syn::spanned::Spanned;
 
@@ -70,12 +70,12 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result<To
                         <#ty as schemars::JsonSchema>::schema_id()
                     }
 
-                    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
-                        <#ty as schemars::JsonSchema>::json_schema(generator)
+                    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_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 {
@@ -188,7 +188,7 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result<To
                     #schema_id
                 }
 
-                fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+                fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
                     #schema_expr
                 }
             };
diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs
deleted file mode 100644
index 0f390c6..0000000
--- a/schemars_derive/src/metadata.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use proc_macro2::TokenStream;
-use syn::{spanned::Spanned, Expr};
-
-#[derive(Debug, Clone)]
-pub struct SchemaMetadata<'a> {
-    pub title: Option<&'a Expr>,
-    pub description: Option<&'a Expr>,
-    pub doc: Option<&'a Expr>,
-    pub deprecated: bool,
-    pub read_only: bool,
-    pub write_only: bool,
-    pub examples: &'a [syn::Path],
-    pub default: Option<TokenStream>,
-    pub extensions: &'a [(String, TokenStream)],
-    pub transforms: &'a [Expr],
-}
-
-impl<'a> SchemaMetadata<'a> {
-    pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
-        let setters = self.make_setters();
-        if !setters.is_empty() || !self.transforms.is_empty() {
-            let apply_transforms = self.transforms.iter().map(|t| {
-                quote_spanned! {t.span()=>
-                    schemars::transform::Transform::transform(&mut #t, &mut schema);
-                }
-            });
-            *schema_expr = quote! {{
-                let mut schema = #schema_expr;
-                #(#setters)*
-                #(#apply_transforms)*
-                schema
-            }}
-        }
-    }
-
-    fn make_setters(&self) -> Vec<TokenStream> {
-        let mut setters = Vec::<TokenStream>::new();
-
-        if let Some(doc) = &self.doc {
-            if self.title.is_none() || self.description.is_none() {
-                setters.push(quote!{
-                    const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc);
-                });
-            }
-        }
-        if let Some(title) = &self.title {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", #title);
-            });
-        } else if self.doc.is_some() {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", title_and_description.0);
-            });
-        }
-        if let Some(description) = &self.description {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", #description);
-            });
-        } else if self.doc.is_some() {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", title_and_description.1);
-            });
-        }
-
-        if self.deprecated {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property(&mut schema, "deprecated", true);
-            });
-        }
-
-        if self.read_only {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property(&mut schema, "readOnly", true);
-            });
-        }
-        if self.write_only {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property(&mut schema, "writeOnly", true);
-            });
-        }
-
-        if !self.examples.is_empty() {
-            let examples = self.examples.iter().map(|eg| {
-                quote! {
-                    schemars::_serde_json::value::to_value(#eg())
-                }
-            });
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property(&mut schema, "examples", schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect()));
-            });
-        }
-
-        if let Some(default) = &self.default {
-            setters.push(quote! {
-                if let Some(default) = #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) {
-                    schemars::_private::insert_metadata_property(&mut schema, "default", default);
-                }
-            });
-        }
-
-        for (k, v) in self.extensions {
-            setters.push(quote! {
-                schemars::_private::insert_metadata_property(&mut schema, #k, schemars::_serde_json::json!(#v));
-            });
-        }
-
-        setters
-    }
-}
diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs
index 5b1eb97..dac1486 100644
--- a/schemars_derive/src/schema_exprs.rs
+++ b/schemars_derive/src/schema_exprs.rs
@@ -1,12 +1,55 @@
-use std::collections::HashSet;
-
-use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata};
+use crate::{ast::*, attr::WithAttr, idents::*};
 use proc_macro2::{Span, TokenStream};
+use quote::ToTokens;
 use serde_derive_internals::ast::Style;
 use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType};
+use std::collections::HashSet;
 use syn::spanned::Spanned;
 
-pub fn expr_for_container(cont: &Container) -> TokenStream {
+pub struct SchemaExpr {
+    /// Definitions for types or functions that may be used within the creator or mutators
+    definitions: Vec<TokenStream>,
+    /// An expression that produces a `Schema`
+    creator: TokenStream,
+    /// Statements (including terminating semicolon) that mutate a var `schema` of type `Schema`
+    mutators: Vec<TokenStream>,
+}
+
+impl From<TokenStream> for SchemaExpr {
+    fn from(creator: TokenStream) -> Self {
+        Self {
+            definitions: Vec::new(),
+            creator,
+            mutators: Vec::new(),
+        }
+    }
+}
+
+impl ToTokens for SchemaExpr {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let Self {
+            definitions,
+            creator,
+            mutators,
+        } = self;
+
+        tokens.extend(if mutators.is_empty() {
+            quote!({
+                #(#definitions)*
+                #creator
+            })
+        } else {
+            quote!({
+                #(#definitions)*
+                let mut #SCHEMA = #creator;
+                #(#mutators)*
+                #SCHEMA
+            })
+        });
+    }
+}
+
+pub fn expr_for_container(cont: &Container) -> SchemaExpr {
     let mut schema_expr = match &cont.data {
         Data::Struct(Style::Unit, _) => expr_for_unit_struct(),
         Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]),
@@ -19,11 +62,11 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
         Data::Enum(variants) => expr_for_enum(variants, &cont.serde_attrs),
     };
 
-    cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
+    cont.add_mutators(&mut schema_expr.mutators);
     schema_expr
 }
 
-pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
+pub fn expr_for_repr(cont: &Container) -> Result<SchemaExpr, syn::Error> {
     let repr_type = cont.attrs.repr.as_ref().ok_or_else(|| {
         syn::Error::new(
             Span::call_site(),
@@ -49,7 +92,7 @@ pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
     let enum_ident = &cont.ident;
     let variant_idents = variants.iter().map(|v| &v.ident);
 
-    let mut schema_expr = quote!({
+    let mut schema_expr = SchemaExpr::from(quote!({
         let mut map = schemars::_serde_json::Map::new();
         map.insert("type".into(), "integer".into());
         map.insert(
@@ -61,33 +104,33 @@ pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
             }),
         );
         schemars::Schema::from(map)
-    });
+    }));
+
+    cont.add_mutators(&mut schema_expr.mutators);
 
-    cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
     Ok(schema_expr)
 }
 
-fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
+fn expr_for_field(field: &Field, allow_ref: bool) -> SchemaExpr {
     let (ty, type_def) = type_for_field_schema(field);
     let span = field.original.span();
-    let generator = quote!(generator);
 
-    let mut schema_expr = if field.validation_attrs.required() {
+    let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required {
         quote_spanned! {span=>
-            <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#generator)
+            <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR)
         }
     } else if allow_ref {
         quote_spanned! {span=>
-            #generator.subschema_for::<#ty>()
+            #GENERATOR.subschema_for::<#ty>()
         }
     } else {
         quote_spanned! {span=>
-            <#ty as schemars::JsonSchema>::json_schema(#generator)
+            <#ty as schemars::JsonSchema>::json_schema(#GENERATOR)
         }
-    };
+    });
 
-    prepend_type_def(type_def, &mut schema_expr);
-    field.validation_attrs.apply_to_schema(&mut schema_expr);
+    schema_expr.definitions.extend(type_def);
+    field.add_mutators(&mut schema_expr.mutators);
 
     schema_expr
 }
@@ -138,7 +181,7 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option<TokenStream>) {
     }
 }
 
-fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream {
+fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> SchemaExpr {
     let deny_unknown_fields = cattrs.deny_unknown_fields();
     let variants = variants
         .iter()
@@ -159,7 +202,7 @@ fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenS
 fn expr_for_external_tagged_enum<'a>(
     variants: impl Iterator<Item = &'a Variant<'a>>,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     let mut unique_names = HashSet::<&str>::new();
     let mut count = 0;
     let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = variants
@@ -169,7 +212,7 @@ fn expr_for_external_tagged_enum<'a>(
         })
         .partition(|v| v.is_unit() && v.attrs.is_default());
     let unit_names = unit_variants.iter().map(|v| v.name());
-    let unit_schema = quote!({
+    let unit_schema = SchemaExpr::from(quote!({
         let mut map = schemars::_serde_json::Map::new();
         map.insert("type".into(), "string".into());
         map.insert(
@@ -181,7 +224,7 @@ fn expr_for_external_tagged_enum<'a>(
             }),
         );
         schemars::Schema::from(map)
-    });
+    }));
 
     if complex_variants.is_empty() {
         return unit_schema;
@@ -195,21 +238,19 @@ fn expr_for_external_tagged_enum<'a>(
     schemas.extend(complex_variants.into_iter().map(|variant| {
         let name = variant.name();
 
-        let mut schema_expr = if variant.is_unit() && variant.attrs.with.is_none() {
-            quote! {
-                schemars::_private::new_unit_enum_variant(#name)
-            }
-        } else {
-            let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
-            quote! {
-                schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema)
-            }
-        };
+        let mut schema_expr =
+            SchemaExpr::from(if variant.is_unit() && variant.attrs.with.is_none() {
+                quote! {
+                    schemars::_private::new_unit_enum_variant(#name)
+                }
+            } else {
+                let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
+                quote! {
+                    schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema)
+                }
+            });
 
-        variant
-            .attrs
-            .as_metadata()
-            .apply_to_schema(&mut schema_expr);
+        variant.add_mutators(&mut schema_expr.mutators);
 
         schema_expr
     }));
@@ -221,7 +262,7 @@ fn expr_for_internal_tagged_enum<'a>(
     variants: impl Iterator<Item = &'a Variant<'a>>,
     tag_name: &str,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     let mut unique_names = HashSet::new();
     let mut count = 0;
     let variant_schemas = variants
@@ -229,15 +270,15 @@ fn expr_for_internal_tagged_enum<'a>(
             unique_names.insert(variant.name());
             count += 1;
 
+            let mut schema_expr = expr_for_internal_tagged_enum_variant(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.mutators);
 
-            let mut schema_expr = expr_for_internal_tagged_enum_variant(variant, deny_unknown_fields);
-            schema_expr = quote!({
-                let mut schema = #schema_expr;
-                schemars::_private::apply_internal_enum_variant_tag(&mut schema, #tag_name, #name, #deny_unknown_fields);
-                schema
-            });
-            variant.attrs.as_metadata().apply_to_schema(&mut schema_expr);
             schema_expr
         })
         .collect();
@@ -248,15 +289,12 @@ fn expr_for_internal_tagged_enum<'a>(
 fn expr_for_untagged_enum<'a>(
     variants: impl Iterator<Item = &'a Variant<'a>>,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     let schemas = variants
         .map(|variant| {
             let mut schema_expr = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
 
-            variant
-                .attrs
-                .as_metadata()
-                .apply_to_schema(&mut schema_expr);
+            variant.add_mutators(&mut schema_expr.mutators);
 
             schema_expr
         })
@@ -272,7 +310,7 @@ fn expr_for_adjacent_tagged_enum<'a>(
     tag_name: &str,
     content_name: &str,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     let mut unique_names = HashSet::new();
     let mut count = 0;
     let schemas = variants
@@ -311,27 +349,22 @@ fn expr_for_adjacent_tagged_enum<'a>(
                 TokenStream::new()
             };
 
-            let mut outer_schema = quote! {
-                schemars::json_schema!({
-                    "type": "object",
-                    "properties": {
-                        #tag_name: (#tag_schema),
-                        #add_content_to_props
-                    },
-                    "required": [
-                        #tag_name,
-                        #add_content_to_required
-                    ],
-                    // As we're creating a "wrapper" object, we can honor the
-                    // disposition of deny_unknown_fields.
-                    #set_additional_properties
-                })
-            };
-
-            variant
-                .attrs
-                .as_metadata()
-                .apply_to_schema(&mut outer_schema);
+            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.mutators);
 
             outer_schema
         })
@@ -342,7 +375,7 @@ fn expr_for_adjacent_tagged_enum<'a>(
 
 /// Callers must determine if all subschemas are mutually exclusive. This can
 /// be done for most tagging regimes by checking that all tag names are unique.
-fn variant_subschemas(unique: bool, schemas: Vec<TokenStream>) -> TokenStream {
+fn variant_subschemas(unique: bool, schemas: Vec<SchemaExpr>) -> SchemaExpr {
     let keyword = if unique { "oneOf" } else { "anyOf" };
     quote!({
         let mut map = schemars::_serde_json::Map::new();
@@ -356,17 +389,18 @@ fn variant_subschemas(unique: bool, schemas: Vec<TokenStream>) -> TokenStream {
         );
         schemars::Schema::from(map)
     })
+    .into()
 }
 
-fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream {
+fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> SchemaExpr {
     if let Some(with_attr) = &variant.attrs.with {
         let (ty, type_def) = type_for_schema(with_attr);
-        let generator = quote!(generator);
-        let mut schema_expr = quote_spanned! {variant.original.span()=>
-            #generator.subschema_for::<#ty>()
-        };
+        let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=>
+            #GENERATOR.subschema_for::<#ty>()
+        });
+
+        schema_expr.definitions.extend(type_def);
 
-        prepend_type_def(type_def, &mut schema_expr);
         return schema_expr;
     }
 
@@ -381,15 +415,15 @@ fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool)
 fn expr_for_internal_tagged_enum_variant(
     variant: &Variant,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     if let Some(with_attr) = &variant.attrs.with {
         let (ty, type_def) = type_for_schema(with_attr);
-        let generator = quote!(generator);
-        let mut schema_expr = quote_spanned! {variant.original.span()=>
-            <#ty as schemars::JsonSchema>::json_schema(#generator)
-        };
+        let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=>
+            <#ty as schemars::JsonSchema>::json_schema(#GENERATOR)
+        });
+
+        schema_expr.definitions.extend(type_def);
 
-        prepend_type_def(type_def, &mut schema_expr);
         return schema_expr;
     }
 
@@ -401,17 +435,18 @@ fn expr_for_internal_tagged_enum_variant(
     }
 }
 
-fn expr_for_unit_struct() -> TokenStream {
+fn expr_for_unit_struct() -> SchemaExpr {
     quote! {
-        generator.subschema_for::<()>()
+        #GENERATOR.subschema_for::<()>()
     }
+    .into()
 }
 
-fn expr_for_newtype_struct(field: &Field) -> TokenStream {
+fn expr_for_newtype_struct(field: &Field) -> SchemaExpr {
     expr_for_field(field, true)
 }
 
-fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
+fn expr_for_tuple_struct(fields: &[Field]) -> SchemaExpr {
     let fields: Vec<_> = fields
         .iter()
         .filter(|f| !f.serde_attrs.skip_deserializing())
@@ -427,74 +462,70 @@ fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
             "maxItems": #len,
         })
     }
+    .into()
 }
 
 fn expr_for_struct(
     fields: &[Field],
     default: &SerdeDefault,
     deny_unknown_fields: bool,
-) -> TokenStream {
+) -> SchemaExpr {
     let set_container_default = match default {
         SerdeDefault::None => None,
-        SerdeDefault::Default => Some(quote!(let container_default = Self::default();)),
-        SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
+        SerdeDefault::Default => Some(quote!(let #STRUCT_DEFAULT = Self::default();)),
+        SerdeDefault::Path(path) => Some(quote!(let #STRUCT_DEFAULT = #path();)),
     };
 
-    let properties: Vec<_> = fields
+    // a vec of mutators
+    let properties: Vec<TokenStream> = 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(field);
 
-                let required = field.validation_attrs.required();
-
-                let args = quote!(generator, #required);
-                let mut schema_expr = quote_spanned! {ty.span()=>
-                    schemars::_private::json_schema_for_flatten::<#ty>(#args)
-                };
+                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)
+                });
 
-                prepend_type_def(type_def, &mut schema_expr);
+                schema_expr.definitions.extend(type_def);
 
                 quote! {
-                    schemars::_private::flatten(&mut schema, #schema_expr);
+                    schemars::_private::flatten(&mut #SCHEMA, #schema_expr);
                 }
             } else {
                 let name = field.name();
-                let default = field_default_expr(field, set_container_default.is_some());
-
                 let (ty, type_def) = type_for_field_schema(field);
 
-                let has_default = default.is_some();
-                let required = field.validation_attrs.required();
-
-                let metadata = SchemaMetadata {
-                    read_only: field.serde_attrs.skip_deserializing(),
-                    write_only: field.serde_attrs.skip_serializing(),
-                    default,
-                    ..field.attrs.as_metadata()
-                };
+                let has_default = set_container_default.is_some() || !field.serde_attrs.default().is_none();
+                let required = field.attrs.validation.required;
 
-                let generator = quote!(generator);
-                let mut schema_expr = if field.validation_attrs.required() {
+                let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required {
                     quote_spanned! {ty.span()=>
-                        <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#generator)
+                        <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR)
                     }
                 } else {
                     quote_spanned! {ty.span()=>
-                        #generator.subschema_for::<#ty>()
+                        #GENERATOR.subschema_for::<#ty>()
                     }
-                };
-
-                metadata.apply_to_schema(&mut schema_expr);
-                field.validation_attrs.apply_to_schema(&mut schema_expr);
+                });
+
+                field.add_mutators(&mut schema_expr.mutators);
+                if let Some(default) = field_default_expr(field, set_container_default.is_some()) {
+                    schema_expr.mutators.push(quote! {
+                        #default.and_then(|d| schemars::_schemars_maybe_to_value!(d))
+                            .map(|d| schemars::_private::insert_metadata_property(&mut #SCHEMA, "default", d));
+                    })
+                }
 
-                quote! {
-                    {
-                        #type_def
-                        schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr);
-                    }
-                }}
+                // embed `#type_def` outside of `#schema_expr`, because it's used as the type param
+                // (i.e. `#type_def` is the definition of `#ty`)
+                quote!({
+                    #type_def
+                    schemars::_private::insert_object_property::<#ty>(&mut #SCHEMA, #name, #has_default, #required, #schema_expr);
+                })
+            }
             })
         .collect();
 
@@ -506,15 +537,14 @@ fn expr_for_struct(
         TokenStream::new()
     };
 
-    quote! ({
-        #set_container_default
-        let mut schema = schemars::json_schema!({
+    SchemaExpr {
+        definitions: set_container_default.into_iter().collect(),
+        creator: quote!(schemars::json_schema!({
             "type": "object",
             #set_additional_properties
-        });
-        #(#properties)*
-        schema
-    })
+        })),
+        mutators: properties,
+    }
 }
 
 fn field_default_expr(field: &Field, container_has_default: bool) -> Option<TokenStream> {
@@ -527,7 +557,7 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option<Toke
     let default_expr = match field_default {
         SerdeDefault::None => {
             let member = &field.member;
-            quote!(container_default.#member)
+            quote!(#STRUCT_DEFAULT.#member)
         }
         SerdeDefault::Default => quote!(<#ty>::default()),
         SerdeDefault::Path(path) => quote!(#path()),
@@ -571,14 +601,3 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option<Toke
         default_expr
     })
 }
-
-fn prepend_type_def(type_def: Option<TokenStream>, schema_expr: &mut TokenStream) {
-    if let Some(type_def) = type_def {
-        *schema_expr = quote! {
-            {
-                #type_def
-                #schema_expr
-            }
-        }
-    }
-}


Test patch:
diff --git a/schemars/tests/expected/skip_tuple_fields.json b/schemars/tests/expected/skip_tuple_fields.json
index c3a4255..e3038bc 100644
--- a/schemars/tests/expected/skip_tuple_fields.json
+++ b/schemars/tests/expected/skip_tuple_fields.json
@@ -5,7 +5,8 @@
   "prefixItems": [
     {
       "type": "number",
-      "format": "float"
+      "format": "float",
+      "writeOnly": true
     },
     {
       "type": "null"
diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json
index 57c7f52..b2c893a 100644
--- a/schemars/tests/expected/validate.json
+++ b/schemars/tests/expected/validate.json
@@ -23,10 +23,6 @@
       "type": "string",
       "pattern": "^[Hh]ello\\b"
     },
-    "regex_str3": {
-      "type": "string",
-      "pattern": "^\\d+$"
-    },
     "contains_str1": {
       "type": "string",
       "pattern": "substring\\.\\.\\."
@@ -39,10 +35,6 @@
       "type": "string",
       "format": "email"
     },
-    "tel": {
-      "type": "string",
-      "format": "phone"
-    },
     "homepage": {
       "type": "string",
       "format": "uri"
@@ -88,11 +80,9 @@
     "min_max2",
     "regex_str1",
     "regex_str2",
-    "regex_str3",
     "contains_str1",
     "contains_str2",
     "email_address",
-    "tel",
     "homepage",
     "non_empty_str",
     "non_empty_str2",
diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json
index 5e7d31d..e258f18 100644
--- a/schemars/tests/expected/validate_schemars_attrs.json
+++ b/schemars/tests/expected/validate_schemars_attrs.json
@@ -20,10 +20,6 @@
       "pattern": "^[Hh]ello\\b"
     },
     "regex_str2": {
-      "type": "string",
-      "pattern": "^[Hh]ello\\b"
-    },
-    "regex_str3": {
       "type": "string",
       "pattern": "^\\d+$"
     },
@@ -39,10 +35,6 @@
       "type": "string",
       "format": "email"
     },
-    "tel": {
-      "type": "string",
-      "format": "phone"
-    },
     "homepage": {
       "type": "string",
       "format": "uri"
@@ -88,11 +80,9 @@
     "min_max2",
     "regex_str1",
     "regex_str2",
-    "regex_str3",
     "contains_str1",
     "contains_str2",
     "email_address",
-    "tel",
     "homepage",
     "non_empty_str",
     "non_empty_str2",
diff --git a/schemars/tests/ui/invalid_validation_attrs.rs b/schemars/tests/ui/invalid_validation_attrs.rs
index 79a4735..4914b70 100644
--- a/schemars/tests/ui/invalid_validation_attrs.rs
+++ b/schemars/tests/ui/invalid_validation_attrs.rs
@@ -1,7 +1,5 @@
 use schemars::JsonSchema;
 
-// FIXME validation attrs like `email` should be disallowed non structs/enums/variants
-
 #[derive(JsonSchema)]
 #[validate(email)]
 pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String);
@@ -15,6 +13,7 @@ pub struct Struct3(
     #[validate(
         regex = "foo",
         contains = "bar",
+        regex(pattern = "baz"),
         regex(path = "baz"),
         phone,
         email,
@@ -29,6 +28,7 @@ pub struct Struct4(
         regex = "foo",
         contains = "bar",
         regex(path = "baz"),
+        regex(pattern = "baz"),
         phone,
         email,
         url
diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr
index 56d4404..684343c 100644
--- a/schemars/tests/ui/invalid_validation_attrs.stderr
+++ b/schemars/tests/ui/invalid_validation_attrs.stderr
@@ -1,53 +1,95 @@
+error: expected validate regex attribute item to be of the form `regex(...)`
+ --> tests/ui/invalid_validation_attrs.rs:5:31
+  |
+5 | pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String);
+  |                               ^^^^^
+
+error: expected schemars regex attribute item to be of the form `regex(...)`
+ --> tests/ui/invalid_validation_attrs.rs:9:31
+  |
+9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
+  |                               ^^^^^
+
+error: schemars attribute cannot contain both `equal` and `min`
+ --> tests/ui/invalid_validation_attrs.rs:9:59
+  |
+9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
+  |                                                           ^^^^^^^^^
+
+error: unknown item in schemars length attribute: `bar`
+ --> tests/ui/invalid_validation_attrs.rs:9:70
+  |
+9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
+  |                                                                      ^^^
+
 error: unknown schemars attribute `foo`
-  --> tests/ui/invalid_validation_attrs.rs:11:38
+ --> tests/ui/invalid_validation_attrs.rs:9:38
+  |
+9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
+  |                                      ^^^
+
+error: unknown schemars attribute `email`
+ --> tests/ui/invalid_validation_attrs.rs:8:12
+  |
+8 | #[schemars(email)]
+  |            ^^^^^
+
+error: expected validate regex attribute item to be of the form `regex(...)`
+  --> tests/ui/invalid_validation_attrs.rs:14:9
    |
-11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
-   |                                      ^^^
+14 |         regex = "foo",
+   |         ^^^^^^^^^^^^^
 
-error: could not parse `regex` item in schemars attribute
-  --> tests/ui/invalid_validation_attrs.rs:11:31
+error: expected validate contains attribute item to be of the form `contains(...)`
+  --> tests/ui/invalid_validation_attrs.rs:15:9
    |
-11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
-   |                               ^^^^^
+15 |         contains = "bar",
+   |         ^^^^^^^^^^^^^^^^
 
-error: schemars attribute cannot contain both `equal` and `min`
-  --> tests/ui/invalid_validation_attrs.rs:11:59
+error: `pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead
+  --> tests/ui/invalid_validation_attrs.rs:16:15
    |
-11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
-   |                                                           ^^^^^
+16 |         regex(pattern = "baz"),
+   |               ^^^^^^^^^^^^^^^
 
-error: unknown item in schemars length attribute
-  --> tests/ui/invalid_validation_attrs.rs:11:70
+error: `validate(regex(...))` attribute requires `path = ...`
+  --> tests/ui/invalid_validation_attrs.rs:16:9
    |
-11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
-   |                                                                      ^^^
+16 |         regex(pattern = "baz"),
+   |         ^^^^^^^^^^^^^^^^^^^^^^
 
-error: schemars attribute cannot contain both `contains` and `regex`
-  --> tests/ui/invalid_validation_attrs.rs:30:9
+error: expected schemars regex attribute item to be of the form `regex(...)`
+  --> tests/ui/invalid_validation_attrs.rs:28:9
    |
-30 |         contains = "bar",
-   |         ^^^^^^^^
+28 |         regex = "foo",
+   |         ^^^^^^^^^^^^^
 
-error: duplicate schemars attribute `regex`
-  --> tests/ui/invalid_validation_attrs.rs:31:9
+error: expected schemars contains attribute item to be of the form `contains(...)`
+  --> tests/ui/invalid_validation_attrs.rs:29:9
    |
-31 |         regex(path = "baz"),
-   |         ^^^^^
+29 |         contains = "bar",
+   |         ^^^^^^^^^^^^^^^^
 
-error: schemars attribute cannot contain both `phone` and `email`
-  --> tests/ui/invalid_validation_attrs.rs:33:9
+error: `path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead
+  --> tests/ui/invalid_validation_attrs.rs:30:15
    |
-33 |         email,
-   |         ^^^^^
+30 |         regex(path = "baz"),
+   |               ^^^^^^^^^^^^
 
-error: schemars attribute cannot contain both `phone` and `url`
+error: `schemars(regex(...))` attribute requires `pattern = ...`
+  --> tests/ui/invalid_validation_attrs.rs:30:9
+   |
+30 |         regex(path = "baz"),
+   |         ^^^^^^^^^^^^^^^^^^^
+
+error: schemars attribute cannot contain both `url` and `email`
   --> tests/ui/invalid_validation_attrs.rs:34:9
    |
 34 |         url
    |         ^^^
 
-error[E0425]: cannot find value `foo` in this scope
-  --> tests/ui/invalid_validation_attrs.rs:16:17
+error: unknown schemars attribute `phone`
+  --> tests/ui/invalid_validation_attrs.rs:32:9
    |
-16 |         regex = "foo",
-   |                 ^^^^^ not found in this scope
+32 |         phone,
+   |         ^^^^^
diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs
index 0d0eefb..9e07fd5 100644
--- a/schemars/tests/validate.rs
+++ b/schemars/tests/validate.rs
@@ -16,20 +16,16 @@ pub struct Struct {
     min_max: f32,
     #[validate(range(min = "MIN", max = "MAX"))]
     min_max2: f32,
-    #[validate(regex = &*STARTS_WITH_HELLO)]
+    #[validate(regex(path = &*STARTS_WITH_HELLO))]
     regex_str1: String,
     #[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
     regex_str2: String,
-    #[validate(regex(pattern = r"^\d+$"))]
-    regex_str3: String,
-    #[validate(contains = "substring...")]
+    #[validate(contains(pattern = "substring..."))]
     contains_str1: String,
     #[validate(contains(pattern = "substring...", message = "bar"))]
     contains_str2: String,
     #[validate(email)]
     email_address: String,
-    #[validate(phone)]
-    tel: String,
     #[validate(url)]
     homepage: String,
     #[validate(length(min = 1, max = 100))]
@@ -38,7 +34,7 @@ pub struct Struct {
     non_empty_str2: String,
     #[validate(length(equal = 2))]
     pair: Vec<i32>,
-    #[validate(contains = "map_key")]
+    #[validate(contains(pattern = "map_key"))]
     map_contains: BTreeMap<String, ()>,
     #[validate(required)]
     required_option: Option<bool>,
@@ -66,22 +62,18 @@ pub struct Struct2 {
     min_max: f32,
     #[schemars(range(min = "MIN", max = "MAX"))]
     min_max2: f32,
-    #[validate(regex = "overridden")]
-    #[schemars(regex = "STARTS_WITH_HELLO")]
+    #[validate(regex(path = overridden))]
+    #[schemars(regex(pattern = &*STARTS_WITH_HELLO))]
     regex_str1: String,
-    #[schemars(regex(path = "STARTS_WITH_HELLO"))]
-    regex_str2: String,
     #[schemars(regex(pattern = r"^\d+$"))]
-    regex_str3: String,
-    #[validate(regex = "overridden")]
-    #[schemars(contains = "substring...")]
+    regex_str2: String,
+    #[validate(contains(pattern = "overridden"))]
+    #[schemars(contains(pattern = "substring..."))]
     contains_str1: String,
     #[schemars(contains(pattern = "substring..."))]
     contains_str2: String,
     #[schemars(email)]
     email_address: String,
-    #[schemars(phone)]
-    tel: String,
     #[schemars(url)]
     homepage: String,
     #[schemars(length(min = 1, max = 100))]
@@ -90,7 +82,7 @@ pub struct Struct2 {
     non_empty_str2: String,
     #[schemars(length(equal = 2))]
     pair: Vec<i32>,
-    #[schemars(contains = "map_key")]
+    #[schemars(contains(pattern = "map_key"))]
     map_contains: BTreeMap<String, ()>,
     #[schemars(required)]
     required_option: Option<bool>,
diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs
index 6416671..d3d8d65 100644
--- a/schemars/tests/validate_inner.rs
+++ b/schemars/tests/validate_inner.rs
@@ -13,7 +13,7 @@ pub struct Struct<'a> {
     array_str_length: [&'a str; 2],
     #[schemars(inner(contains(pattern = "substring...")))]
     slice_str_contains: &'a [&'a str],
-    #[schemars(inner(regex = "STARTS_WITH_HELLO"))]
+    #[schemars(inner(regex(pattern = STARTS_WITH_HELLO)))]
     vec_str_regex: Vec<String>,
     #[schemars(inner(length(min = 1, max = 100)))]
     vec_str_length: Vec<&'a str>,