about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJacob Pratt <jacob@jhpratt.dev>2025-09-13 03:26:01 -0400
committerGitHub <noreply@github.com>2025-09-13 03:26:01 -0400
commit544644476d9c304648801dae76d85b1c17fc35d1 (patch)
treeceeb6ffcfbe92d361b50563382d416df6a2663d3
parent9642c0ef6749e57ce76ac153807df1cc38cd26ee (diff)
parentf157ce994ea45e9faea9eff89c5f8b3d4ea77b6e (diff)
downloadrust-544644476d9c304648801dae76d85b1c17fc35d1.tar.gz
rust-544644476d9c304648801dae76d85b1c17fc35d1.zip
Rollup merge of #144498 - Noratrieb:rustc-json-schema, r=jieyouxu,davidtwco
Add --print target-spec-json-schema

This schema is helpful for people writing custom target spec JSON. It can provide autocomplete in the editor, and also serves as documentation when there are documentation comments on the structs, as `schemars` will put them in the schema.

I was motivated to do this because I saw someone write their own version of this schema by hand, so demand for this clearly exists. It's not a lot of effort to implement, so I thought it would make sense.

MCP: https://github.com/rust-lang/compiler-team/issues/905

I think it would also be useful to put this in the sysroot in `etc` so people can link it directly in their editors.

I would have loved to add a test that validates the JSON schema against the spec JSON of every builtin target, but I don't want to do it as the JSON schema validation crates have incredible amounts of dependencies because JSON schema supports a ton of random features. I don't want to add that, even as a dev dependency.
-rw-r--r--Cargo.lock63
-rw-r--r--compiler/rustc_codegen_ssa/src/back/command.rs2
-rw-r--r--compiler/rustc_driver_impl/src/lib.rs4
-rw-r--r--compiler/rustc_session/src/config.rs5
-rw-r--r--compiler/rustc_target/Cargo.toml1
-rw-r--r--compiler/rustc_target/src/lib.rs62
-rw-r--r--compiler/rustc_target/src/spec/json.rs38
-rw-r--r--compiler/rustc_target/src/spec/mod.rs969
-rw-r--r--src/bootstrap/src/core/build_steps/dist.rs24
-rw-r--r--src/bootstrap/src/utils/proc_macro_deps.rs1
-rw-r--r--src/doc/rustc/src/targets/custom.md15
-rw-r--r--src/tools/tidy/src/deps.rs6
-rw-r--r--tests/run-make/print-request-help-stable-unstable/help-diff.diff2
-rw-r--r--tests/run-make/print-request-help-stable-unstable/unstable-invalid-print-request-help.err2
-rw-r--r--tests/run-make/rustc-help/help-v.stdout2
-rw-r--r--tests/run-make/rustc-help/help.stdout2
-rw-r--r--tests/ui/invalid-compile-flags/print-without-arg.stderr2
-rw-r--r--tests/ui/invalid-compile-flags/print.stderr2
-rw-r--r--tests/ui/print-request/print-lints-help.stderr2
19 files changed, 473 insertions, 731 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2dc6e8ff103..4677d34d2a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1192,6 +1192,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921"
 
 [[package]]
+name = "dyn-clone"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+
+[[package]]
 name = "either"
 version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3123,6 +3129,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
 name = "regex"
 version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4577,6 +4603,7 @@ dependencies = [
  "rustc_macros",
  "rustc_serialize",
  "rustc_span",
+ "schemars",
  "serde",
  "serde_derive",
  "serde_json",
@@ -4901,6 +4928,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "schemars"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.106",
+]
+
+[[package]]
 name = "scoped-tls"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4975,6 +5027,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
 name = "serde_json"
 version = "1.0.142"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/compiler/rustc_codegen_ssa/src/back/command.rs b/compiler/rustc_codegen_ssa/src/back/command.rs
index 05351bd6ca3..7420f18aacb 100644
--- a/compiler/rustc_codegen_ssa/src/back/command.rs
+++ b/compiler/rustc_codegen_ssa/src/back/command.rs
@@ -109,7 +109,7 @@ impl Command {
             }
             Program::Lld(ref p, flavor) => {
                 let mut c = process::Command::new(p);
-                c.arg("-flavor").arg(flavor.as_str());
+                c.arg("-flavor").arg(flavor.desc());
                 c
             }
         };
diff --git a/compiler/rustc_driver_impl/src/lib.rs b/compiler/rustc_driver_impl/src/lib.rs
index d00a4c35834..4f875cf99ec 100644
--- a/compiler/rustc_driver_impl/src/lib.rs
+++ b/compiler/rustc_driver_impl/src/lib.rs
@@ -668,6 +668,10 @@ fn print_crate_info(
             TargetSpecJson => {
                 println_info!("{}", serde_json::to_string_pretty(&sess.target.to_json()).unwrap());
             }
+            TargetSpecJsonSchema => {
+                let schema = rustc_target::spec::json_schema();
+                println_info!("{}", serde_json::to_string_pretty(&schema).unwrap());
+            }
             AllTargetSpecsJson => {
                 let mut targets = BTreeMap::new();
                 for name in rustc_target::spec::TARGETS {
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 9793d8091e2..297df7c2c97 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -70,6 +70,7 @@ pub const PRINT_KINDS: &[(&str, PrintKind)] = &[
     ("target-libdir", PrintKind::TargetLibdir),
     ("target-list", PrintKind::TargetList),
     ("target-spec-json", PrintKind::TargetSpecJson),
+    ("target-spec-json-schema", PrintKind::TargetSpecJsonSchema),
     ("tls-models", PrintKind::TlsModels),
     // tidy-alphabetical-end
 ];
@@ -1043,6 +1044,7 @@ pub enum PrintKind {
     TargetLibdir,
     TargetList,
     TargetSpecJson,
+    TargetSpecJsonSchema,
     TlsModels,
     // tidy-alphabetical-end
 }
@@ -2323,7 +2325,8 @@ fn is_print_request_stable(print_kind: PrintKind) -> bool {
         | PrintKind::CheckCfg
         | PrintKind::CrateRootLintLevels
         | PrintKind::SupportedCrateTypes
-        | PrintKind::TargetSpecJson => false,
+        | PrintKind::TargetSpecJson
+        | PrintKind::TargetSpecJsonSchema => false,
         _ => true,
     }
 }
diff --git a/compiler/rustc_target/Cargo.toml b/compiler/rustc_target/Cargo.toml
index 7fabb227d9a..ecdb6ab5a57 100644
--- a/compiler/rustc_target/Cargo.toml
+++ b/compiler/rustc_target/Cargo.toml
@@ -14,6 +14,7 @@ rustc_fs_util = { path = "../rustc_fs_util" }
 rustc_macros = { path = "../rustc_macros" }
 rustc_serialize = { path = "../rustc_serialize" }
 rustc_span = { path = "../rustc_span" }
+schemars = "1.0.4"
 serde = "1.0.219"
 serde_derive = "1.0.219"
 serde_json = "1.0.59"
diff --git a/compiler/rustc_target/src/lib.rs b/compiler/rustc_target/src/lib.rs
index 91657fef803..b3fe1fffcce 100644
--- a/compiler/rustc_target/src/lib.rs
+++ b/compiler/rustc_target/src/lib.rs
@@ -72,3 +72,65 @@ fn find_relative_libdir(sysroot: &Path) -> std::borrow::Cow<'static, str> {
         Some(libdir) => libdir.into(),
     }
 }
+
+macro_rules! target_spec_enum {
+    (
+        $( #[$attr:meta] )*
+        pub enum $name:ident {
+            $(
+                $( #[$variant_attr:meta] )*
+                $variant:ident = $string:literal,
+            )*
+        }
+        parse_error_type = $parse_error_type:literal;
+    ) => {
+        $( #[$attr] )*
+        #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
+        #[derive(schemars::JsonSchema)]
+        pub enum $name {
+            $(
+                $( #[$variant_attr] )*
+                #[serde(rename = $string)] // for JSON schema generation only
+                $variant,
+            )*
+        }
+
+        impl FromStr for $name {
+            type Err = String;
+
+            fn from_str(s: &str) -> Result<Self, Self::Err> {
+                Ok(match s {
+                    $( $string => Self::$variant, )*
+                    _ => {
+                        let all = [$( concat!("'", $string, "'") ),*].join(", ");
+                        return Err(format!("invalid {}: '{s}'. allowed values: {all}", $parse_error_type));
+                    }
+                })
+            }
+        }
+
+        impl $name {
+            pub fn desc(&self) -> &'static str {
+                match self {
+                    $( Self::$variant => $string, )*
+                }
+            }
+        }
+
+        impl crate::json::ToJson for $name {
+            fn to_json(&self) -> crate::json::Json {
+                self.desc().to_json()
+            }
+        }
+
+        crate::json::serde_deserialize_from_str!($name);
+
+
+        impl std::fmt::Display for $name {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                f.write_str(self.desc())
+            }
+        }
+    };
+}
+use target_spec_enum;
diff --git a/compiler/rustc_target/src/spec/json.rs b/compiler/rustc_target/src/spec/json.rs
index e9ae5734d5b..f236be92b3b 100644
--- a/compiler/rustc_target/src/spec/json.rs
+++ b/compiler/rustc_target/src/spec/json.rs
@@ -408,12 +408,12 @@ impl ToJson for Target {
     }
 }
 
-#[derive(serde_derive::Deserialize)]
+#[derive(serde_derive::Deserialize, schemars::JsonSchema)]
 struct LinkSelfContainedComponentsWrapper {
     components: Vec<LinkSelfContainedComponents>,
 }
 
-#[derive(serde_derive::Deserialize)]
+#[derive(serde_derive::Deserialize, schemars::JsonSchema)]
 #[serde(untagged)]
 enum TargetFamiliesJson {
     Array(StaticCow<[StaticCow<str>]>),
@@ -429,6 +429,18 @@ impl FromStr for EndianWrapper {
     }
 }
 crate::json::serde_deserialize_from_str!(EndianWrapper);
+impl schemars::JsonSchema for EndianWrapper {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "Endian".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": ["big", "little"]
+        })
+        .into()
+    }
+}
 
 /// `ExternAbi` is in `rustc_abi`, which doesn't have access to the macro and serde.
 struct ExternAbiWrapper(rustc_abi::ExternAbi);
@@ -441,8 +453,22 @@ impl FromStr for ExternAbiWrapper {
     }
 }
 crate::json::serde_deserialize_from_str!(ExternAbiWrapper);
+impl schemars::JsonSchema for ExternAbiWrapper {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "ExternAbi".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let all =
+            rustc_abi::ExternAbi::ALL_VARIANTS.iter().map(|abi| abi.as_str()).collect::<Vec<_>>();
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": all,
+        })
+        .into()
+    }
+}
 
-#[derive(serde_derive::Deserialize)]
+#[derive(serde_derive::Deserialize, schemars::JsonSchema)]
 struct TargetSpecJsonMetadata {
     description: Option<StaticCow<str>>,
     tier: Option<u64>,
@@ -450,7 +476,7 @@ struct TargetSpecJsonMetadata {
     std: Option<bool>,
 }
 
-#[derive(serde_derive::Deserialize)]
+#[derive(serde_derive::Deserialize, schemars::JsonSchema)]
 #[serde(rename_all = "kebab-case")]
 // Ensure that all unexpected fields get turned into errors.
 // This helps users stay up to date when the schema changes instead of silently
@@ -593,3 +619,7 @@ struct TargetSpecJson {
     supports_xray: Option<bool>,
     entry_abi: Option<ExternAbiWrapper>,
 }
+
+pub fn json_schema() -> schemars::Schema {
+    schemars::schema_for!(TargetSpecJson)
+}
diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs
index 4d9f06c568b..07fb1ce63f7 100644
--- a/compiler/rustc_target/src/spec/mod.rs
+++ b/compiler/rustc_target/src/spec/mod.rs
@@ -70,6 +70,7 @@ mod json;
 pub use abi_map::{AbiMap, AbiMapping};
 pub use base::apple;
 pub use base::avr::ef_avr_arch;
+pub use json::json_schema;
 
 /// Linker is called through a C/C++ compiler.
 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -183,50 +184,15 @@ impl LinkerFlavorCli {
     }
 }
 
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
-pub enum LldFlavor {
-    Wasm,
-    Ld64,
-    Ld,
-    Link,
-}
-
-impl LldFlavor {
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            LldFlavor::Wasm => "wasm",
-            LldFlavor::Ld64 => "darwin",
-            LldFlavor::Ld => "gnu",
-            LldFlavor::Link => "link",
-        }
-    }
-}
-
-impl FromStr for LldFlavor {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "darwin" => LldFlavor::Ld64,
-            "gnu" => LldFlavor::Ld,
-            "link" => LldFlavor::Link,
-            "wasm" => LldFlavor::Wasm,
-            _ => {
-                return Err(
-                    "invalid value for lld flavor: '{s}', expected one of 'darwin', 'gnu', 'link', 'wasm'"
-                        .into(),
-                );
-            }
-        })
+crate::target_spec_enum! {
+    pub enum LldFlavor {
+        Wasm = "wasm",
+        Ld64 = "darwin",
+        Ld = "gnu",
+        Link = "link",
     }
-}
 
-crate::json::serde_deserialize_from_str!(LldFlavor);
-
-impl ToJson for LldFlavor {
-    fn to_json(&self) -> Json {
-        self.as_str().to_json()
-    }
+    parse_error_type = "LLD flavor";
 }
 
 impl LinkerFlavor {
@@ -558,6 +524,20 @@ linker_flavor_cli_impls! {
 }
 
 crate::json::serde_deserialize_from_str!(LinkerFlavorCli);
+impl schemars::JsonSchema for LinkerFlavorCli {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "LinkerFlavor".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let all: Vec<&'static str> =
+            Self::all().iter().map(|flavor| flavor.desc()).collect::<Vec<_>>();
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": all
+        })
+        .into()
+    }
+}
 
 impl ToJson for LinkerFlavorCli {
     fn to_json(&self) -> Json {
@@ -611,6 +591,18 @@ impl FromStr for LinkSelfContainedDefault {
 }
 
 crate::json::serde_deserialize_from_str!(LinkSelfContainedDefault);
+impl schemars::JsonSchema for LinkSelfContainedDefault {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "LinkSelfContainedDefault".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": ["false", "true", "wasm", "musl", "mingw"]
+        })
+        .into()
+    }
+}
 
 impl ToJson for LinkSelfContainedDefault {
     fn to_json(&self) -> Json {
@@ -743,6 +735,20 @@ impl FromStr for LinkSelfContainedComponents {
 }
 
 crate::json::serde_deserialize_from_str!(LinkSelfContainedComponents);
+impl schemars::JsonSchema for LinkSelfContainedComponents {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "LinkSelfContainedComponents".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let all =
+            Self::all_components().iter().map(|component| component.as_str()).collect::<Vec<_>>();
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": all,
+        })
+        .into()
+    }
+}
 
 impl ToJson for LinkSelfContainedComponents {
     fn to_json(&self) -> Json {
@@ -823,10 +829,14 @@ impl LinkerFeatures {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Hash, Encodable, Decodable, HashStable_Generic)]
-pub enum PanicStrategy {
-    Unwind,
-    Abort,
+crate::target_spec_enum! {
+    #[derive(Encodable, Decodable, HashStable_Generic)]
+    pub enum PanicStrategy {
+        Unwind = "unwind",
+        Abort = "abort",
+    }
+
+    parse_error_type = "panic strategy";
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Hash, Encodable, Decodable, HashStable_Generic)]
@@ -838,13 +848,6 @@ pub enum OnBrokenPipe {
 }
 
 impl PanicStrategy {
-    pub fn desc(&self) -> &str {
-        match *self {
-            PanicStrategy::Unwind => "unwind",
-            PanicStrategy::Abort => "abort",
-        }
-    }
-
     pub const fn desc_symbol(&self) -> Symbol {
         match *self {
             PanicStrategy::Unwind => sym::unwind,
@@ -857,24 +860,16 @@ impl PanicStrategy {
     }
 }
 
-impl FromStr for PanicStrategy {
-    type Err = String;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "unwind" => PanicStrategy::Unwind,
-            "abort" => PanicStrategy::Abort,
-            _ => {
-                return Err(format!(
-                    "'{}' is not a valid value for \
-                    panic-strategy. Use 'unwind' or 'abort'.",
-                    s
-                ));
-            }
-        })
+crate::target_spec_enum! {
+    pub enum RelroLevel {
+        Full = "full",
+        Partial = "partial",
+        Off = "off",
+        None = "none",
     }
-}
 
-crate::json::serde_deserialize_from_str!(PanicStrategy);
+    parse_error_type = "relro level";
+}
 
 impl IntoDiagArg for PanicStrategy {
     fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
@@ -882,109 +877,14 @@ impl IntoDiagArg for PanicStrategy {
     }
 }
 
-impl ToJson for PanicStrategy {
-    fn to_json(&self) -> Json {
-        match *self {
-            PanicStrategy::Abort => "abort".to_json(),
-            PanicStrategy::Unwind => "unwind".to_json(),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Hash)]
-pub enum RelroLevel {
-    Full,
-    Partial,
-    Off,
-    None,
-}
-
-impl RelroLevel {
-    pub fn desc(&self) -> &str {
-        match *self {
-            RelroLevel::Full => "full",
-            RelroLevel::Partial => "partial",
-            RelroLevel::Off => "off",
-            RelroLevel::None => "none",
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Hash)]
-pub enum SymbolVisibility {
-    Hidden,
-    Protected,
-    Interposable,
-}
-
-impl SymbolVisibility {
-    pub fn desc(&self) -> &str {
-        match *self {
-            SymbolVisibility::Hidden => "hidden",
-            SymbolVisibility::Protected => "protected",
-            SymbolVisibility::Interposable => "interposable",
-        }
-    }
-}
-
-impl FromStr for SymbolVisibility {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<SymbolVisibility, Self::Err> {
-        match s {
-            "hidden" => Ok(SymbolVisibility::Hidden),
-            "protected" => Ok(SymbolVisibility::Protected),
-            "interposable" => Ok(SymbolVisibility::Interposable),
-            _ => Err(format!(
-                "'{}' is not a valid value for \
-                    symbol-visibility. Use 'hidden', 'protected, or 'interposable'.",
-                s
-            )),
-        }
-    }
-}
-
-crate::json::serde_deserialize_from_str!(SymbolVisibility);
-
-impl ToJson for SymbolVisibility {
-    fn to_json(&self) -> Json {
-        match *self {
-            SymbolVisibility::Hidden => "hidden".to_json(),
-            SymbolVisibility::Protected => "protected".to_json(),
-            SymbolVisibility::Interposable => "interposable".to_json(),
-        }
-    }
-}
-
-impl FromStr for RelroLevel {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<RelroLevel, Self::Err> {
-        match s {
-            "full" => Ok(RelroLevel::Full),
-            "partial" => Ok(RelroLevel::Partial),
-            "off" => Ok(RelroLevel::Off),
-            "none" => Ok(RelroLevel::None),
-            _ => Err(format!(
-                "'{}' is not a valid value for \
-                        relro-level. Use 'full', 'partial, 'off', or 'none'.",
-                s
-            )),
-        }
+crate::target_spec_enum! {
+    pub enum SymbolVisibility {
+        Hidden = "hidden",
+        Protected = "protected",
+        Interposable = "interposable",
     }
-}
-
-crate::json::serde_deserialize_from_str!(RelroLevel);
 
-impl ToJson for RelroLevel {
-    fn to_json(&self) -> Json {
-        match *self {
-            RelroLevel::Full => "full".to_json(),
-            RelroLevel::Partial => "partial".to_json(),
-            RelroLevel::Off => "off".to_json(),
-            RelroLevel::None => "None".to_json(),
-        }
-    }
+    parse_error_type = "symbol visibility";
 }
 
 #[derive(Clone, Debug, PartialEq, Hash)]
@@ -1014,6 +914,18 @@ impl FromStr for SmallDataThresholdSupport {
 }
 
 crate::json::serde_deserialize_from_str!(SmallDataThresholdSupport);
+impl schemars::JsonSchema for SmallDataThresholdSupport {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "SmallDataThresholdSupport".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        schemars::json_schema! ({
+            "type": "string",
+            "pattern": r#"^none|default-for-arch|llvm-module-flag=.+|llvm-arg=.+$"#,
+        })
+        .into()
+    }
+}
 
 impl ToJson for SmallDataThresholdSupport {
     fn to_json(&self) -> Value {
@@ -1026,76 +938,31 @@ impl ToJson for SmallDataThresholdSupport {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Hash)]
-pub enum MergeFunctions {
-    Disabled,
-    Trampolines,
-    Aliases,
-}
-
-impl MergeFunctions {
-    pub fn desc(&self) -> &str {
-        match *self {
-            MergeFunctions::Disabled => "disabled",
-            MergeFunctions::Trampolines => "trampolines",
-            MergeFunctions::Aliases => "aliases",
-        }
+crate::target_spec_enum! {
+    pub enum MergeFunctions {
+        Disabled = "disabled",
+        Trampolines = "trampolines",
+        Aliases = "aliases",
     }
-}
-
-impl FromStr for MergeFunctions {
-    type Err = String;
 
-    fn from_str(s: &str) -> Result<MergeFunctions, Self::Err> {
-        match s {
-            "disabled" => Ok(MergeFunctions::Disabled),
-            "trampolines" => Ok(MergeFunctions::Trampolines),
-            "aliases" => Ok(MergeFunctions::Aliases),
-            _ => Err(format!(
-                "'{}' is not a valid value for \
-                    merge-functions. Use 'disabled', \
-                    'trampolines', or 'aliases'.",
-                s
-            )),
-        }
-    }
+    parse_error_type = "value for merge-functions";
 }
 
-crate::json::serde_deserialize_from_str!(MergeFunctions);
-
-impl ToJson for MergeFunctions {
-    fn to_json(&self) -> Json {
-        match *self {
-            MergeFunctions::Disabled => "disabled".to_json(),
-            MergeFunctions::Trampolines => "trampolines".to_json(),
-            MergeFunctions::Aliases => "aliases".to_json(),
-        }
+crate::target_spec_enum! {
+    pub enum RelocModel {
+        Static = "static",
+        Pic = "pic",
+        Pie = "pie",
+        DynamicNoPic = "dynamic-no-pic",
+        Ropi = "ropi",
+        Rwpi = "rwpi",
+        RopiRwpi = "ropi-rwpi",
     }
-}
 
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum RelocModel {
-    Static,
-    Pic,
-    Pie,
-    DynamicNoPic,
-    Ropi,
-    Rwpi,
-    RopiRwpi,
+    parse_error_type = "relocation model";
 }
 
 impl RelocModel {
-    pub fn desc(&self) -> &str {
-        match *self {
-            RelocModel::Static => "static",
-            RelocModel::Pic => "pic",
-            RelocModel::Pie => "pie",
-            RelocModel::DynamicNoPic => "dynamic-no-pic",
-            RelocModel::Ropi => "ropi",
-            RelocModel::Rwpi => "rwpi",
-            RelocModel::RopiRwpi => "ropi-rwpi",
-        }
-    }
     pub const fn desc_symbol(&self) -> Symbol {
         match *self {
             RelocModel::Static => kw::Static,
@@ -1121,236 +988,75 @@ impl RelocModel {
     }
 }
 
-impl FromStr for RelocModel {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<RelocModel, Self::Err> {
-        Ok(match s {
-            "static" => RelocModel::Static,
-            "pic" => RelocModel::Pic,
-            "pie" => RelocModel::Pie,
-            "dynamic-no-pic" => RelocModel::DynamicNoPic,
-            "ropi" => RelocModel::Ropi,
-            "rwpi" => RelocModel::Rwpi,
-            "ropi-rwpi" => RelocModel::RopiRwpi,
-            _ => {
-                return Err(format!(
-                    "invalid relocation model '{s}'.
-                        Run `rustc --print relocation-models` to \
-                        see the list of supported values.'"
-                ));
-            }
-        })
+crate::target_spec_enum! {
+    pub enum CodeModel {
+        Tiny = "tiny",
+        Small = "small",
+        Kernel = "kernel",
+        Medium = "medium",
+        Large = "large",
     }
-}
 
-crate::json::serde_deserialize_from_str!(RelocModel);
-
-impl ToJson for RelocModel {
-    fn to_json(&self) -> Json {
-        self.desc().to_json()
-    }
-}
-
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum CodeModel {
-    Tiny,
-    Small,
-    Kernel,
-    Medium,
-    Large,
+    parse_error_type = "code model";
 }
 
-impl FromStr for CodeModel {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<CodeModel, Self::Err> {
-        Ok(match s {
-            "tiny" => CodeModel::Tiny,
-            "small" => CodeModel::Small,
-            "kernel" => CodeModel::Kernel,
-            "medium" => CodeModel::Medium,
-            "large" => CodeModel::Large,
-            _ => {
-                return Err(format!(
-                    "'{s}' is not a valid code model. \
-                        Run `rustc --print code-models` to \
-                        see the list of supported values."
-                ));
-            }
-        })
+crate::target_spec_enum! {
+    /// The float ABI setting to be configured in the LLVM target machine.
+    pub enum FloatAbi {
+        Soft = "soft",
+        Hard = "hard",
     }
-}
 
-crate::json::serde_deserialize_from_str!(CodeModel);
-
-impl ToJson for CodeModel {
-    fn to_json(&self) -> Json {
-        match *self {
-            CodeModel::Tiny => "tiny",
-            CodeModel::Small => "small",
-            CodeModel::Kernel => "kernel",
-            CodeModel::Medium => "medium",
-            CodeModel::Large => "large",
-        }
-        .to_json()
-    }
+    parse_error_type = "float abi";
 }
 
-/// The float ABI setting to be configured in the LLVM target machine.
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum FloatAbi {
-    Soft,
-    Hard,
-}
-
-impl FromStr for FloatAbi {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<FloatAbi, Self::Err> {
-        Ok(match s {
-            "soft" => FloatAbi::Soft,
-            "hard" => FloatAbi::Hard,
-            _ => {
-                return Err(format!(
-                    "'{}' is not a valid value for \
-                        llvm-floatabi. Use 'soft' or 'hard'.",
-                    s
-                ));
-            }
-        })
+crate::target_spec_enum! {
+    /// The Rustc-specific variant of the ABI used for this target.
+    pub enum RustcAbi {
+        /// On x86-32 only: make use of SSE and SSE2 for ABI purposes.
+        X86Sse2 = "x86-sse2",
+        /// On x86-32/64 only: do not use any FPU or SIMD registers for the ABI.
+        X86Softfloat = "x86-softfloat",
     }
-}
-
-crate::json::serde_deserialize_from_str!(FloatAbi);
 
-impl ToJson for FloatAbi {
-    fn to_json(&self) -> Json {
-        match *self {
-            FloatAbi::Soft => "soft",
-            FloatAbi::Hard => "hard",
-        }
-        .to_json()
-    }
-}
-
-/// The Rustc-specific variant of the ABI used for this target.
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum RustcAbi {
-    /// On x86-32 only: make use of SSE and SSE2 for ABI purposes.
-    X86Sse2,
-    /// On x86-32/64 only: do not use any FPU or SIMD registers for the ABI.
-    X86Softfloat,
+    parse_error_type = "rustc abi";
 }
 
-impl FromStr for RustcAbi {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<RustcAbi, Self::Err> {
-        Ok(match s {
-            "x86-sse2" => RustcAbi::X86Sse2,
-            "x86-softfloat" => RustcAbi::X86Softfloat,
-            _ => {
-                return Err(format!(
-                    "'{s}' is not a valid value for rustc-abi. \
-                        Use 'x86-softfloat' or leave the field unset."
-                ));
-            }
-        })
-    }
-}
-
-crate::json::serde_deserialize_from_str!(RustcAbi);
-
-impl ToJson for RustcAbi {
-    fn to_json(&self) -> Json {
-        match *self {
-            RustcAbi::X86Sse2 => "x86-sse2",
-            RustcAbi::X86Softfloat => "x86-softfloat",
-        }
-        .to_json()
+crate::target_spec_enum! {
+    pub enum TlsModel {
+        GeneralDynamic = "global-dynamic",
+        LocalDynamic = "local-dynamic",
+        InitialExec = "initial-exec",
+        LocalExec = "local-exec",
+        Emulated = "emulated",
     }
-}
 
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum TlsModel {
-    GeneralDynamic,
-    LocalDynamic,
-    InitialExec,
-    LocalExec,
-    Emulated,
-}
-
-impl FromStr for TlsModel {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<TlsModel, Self::Err> {
-        Ok(match s {
-            // Note the difference "general" vs "global" difference. The model name is "general",
-            // but the user-facing option name is "global" for consistency with other compilers.
-            "global-dynamic" => TlsModel::GeneralDynamic,
-            "local-dynamic" => TlsModel::LocalDynamic,
-            "initial-exec" => TlsModel::InitialExec,
-            "local-exec" => TlsModel::LocalExec,
-            "emulated" => TlsModel::Emulated,
-            _ => {
-                return Err(format!(
-                    "'{s}' is not a valid TLS model. \
-                        Run `rustc --print tls-models` to \
-                        see the list of supported values."
-                ));
-            }
-        })
-    }
+    parse_error_type = "TLS model";
 }
 
-crate::json::serde_deserialize_from_str!(TlsModel);
-
-impl ToJson for TlsModel {
-    fn to_json(&self) -> Json {
-        match *self {
-            TlsModel::GeneralDynamic => "global-dynamic",
-            TlsModel::LocalDynamic => "local-dynamic",
-            TlsModel::InitialExec => "initial-exec",
-            TlsModel::LocalExec => "local-exec",
-            TlsModel::Emulated => "emulated",
-        }
-        .to_json()
+crate::target_spec_enum! {
+    /// Everything is flattened to a single enum to make the json encoding/decoding less annoying.
+    pub enum LinkOutputKind {
+        /// Dynamically linked non position-independent executable.
+        DynamicNoPicExe = "dynamic-nopic-exe",
+        /// Dynamically linked position-independent executable.
+        DynamicPicExe = "dynamic-pic-exe",
+        /// Statically linked non position-independent executable.
+        StaticNoPicExe = "static-nopic-exe",
+        /// Statically linked position-independent executable.
+        StaticPicExe = "static-pic-exe",
+        /// Regular dynamic library ("dynamically linked").
+        DynamicDylib = "dynamic-dylib",
+        /// Dynamic library with bundled libc ("statically linked").
+        StaticDylib = "static-dylib",
+        /// WASI module with a lifetime past the _initialize entry point
+        WasiReactorExe = "wasi-reactor-exe",
     }
-}
 
-/// Everything is flattened to a single enum to make the json encoding/decoding less annoying.
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
-pub enum LinkOutputKind {
-    /// Dynamically linked non position-independent executable.
-    DynamicNoPicExe,
-    /// Dynamically linked position-independent executable.
-    DynamicPicExe,
-    /// Statically linked non position-independent executable.
-    StaticNoPicExe,
-    /// Statically linked position-independent executable.
-    StaticPicExe,
-    /// Regular dynamic library ("dynamically linked").
-    DynamicDylib,
-    /// Dynamic library with bundled libc ("statically linked").
-    StaticDylib,
-    /// WASI module with a lifetime past the _initialize entry point
-    WasiReactorExe,
+    parse_error_type = "CRT object kind";
 }
 
 impl LinkOutputKind {
-    fn as_str(&self) -> &'static str {
-        match self {
-            LinkOutputKind::DynamicNoPicExe => "dynamic-nopic-exe",
-            LinkOutputKind::DynamicPicExe => "dynamic-pic-exe",
-            LinkOutputKind::StaticNoPicExe => "static-nopic-exe",
-            LinkOutputKind::StaticPicExe => "static-pic-exe",
-            LinkOutputKind::DynamicDylib => "dynamic-dylib",
-            LinkOutputKind::StaticDylib => "static-dylib",
-            LinkOutputKind::WasiReactorExe => "wasi-reactor-exe",
-        }
-    }
-
     pub fn can_link_dylib(self) -> bool {
         match self {
             LinkOutputKind::StaticNoPicExe | LinkOutputKind::StaticPicExe => false,
@@ -1363,171 +1069,64 @@ impl LinkOutputKind {
     }
 }
 
-impl FromStr for LinkOutputKind {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<LinkOutputKind, Self::Err> {
-        Ok(match s {
-            "dynamic-nopic-exe" => LinkOutputKind::DynamicNoPicExe,
-            "dynamic-pic-exe" => LinkOutputKind::DynamicPicExe,
-            "static-nopic-exe" => LinkOutputKind::StaticNoPicExe,
-            "static-pic-exe" => LinkOutputKind::StaticPicExe,
-            "dynamic-dylib" => LinkOutputKind::DynamicDylib,
-            "static-dylib" => LinkOutputKind::StaticDylib,
-            "wasi-reactor-exe" => LinkOutputKind::WasiReactorExe,
-            _ => {
-                return Err(format!(
-                    "invalid value for CRT object kind. \
-                        Use '(dynamic,static)-(nopic,pic)-exe' or \
-                        '(dynamic,static)-dylib' or 'wasi-reactor-exe'"
-                ));
-            }
-        })
-    }
-}
-
-crate::json::serde_deserialize_from_str!(LinkOutputKind);
-
-impl fmt::Display for LinkOutputKind {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_str(self.as_str())
-    }
-}
-
 pub type LinkArgs = BTreeMap<LinkerFlavor, Vec<StaticCow<str>>>;
 pub type LinkArgsCli = BTreeMap<LinkerFlavorCli, Vec<StaticCow<str>>>;
 
-/// Which kind of debuginfo does the target use?
-///
-/// Useful in determining whether a target supports Split DWARF (a target with
-/// `DebuginfoKind::Dwarf` and supporting `SplitDebuginfo::Unpacked` for example).
-#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
-pub enum DebuginfoKind {
-    /// DWARF debuginfo (such as that used on `x86_64_unknown_linux_gnu`).
-    #[default]
-    Dwarf,
-    /// DWARF debuginfo in dSYM files (such as on Apple platforms).
-    DwarfDsym,
-    /// Program database files (such as on Windows).
-    Pdb,
-}
-
-impl DebuginfoKind {
-    fn as_str(&self) -> &'static str {
-        match self {
-            DebuginfoKind::Dwarf => "dwarf",
-            DebuginfoKind::DwarfDsym => "dwarf-dsym",
-            DebuginfoKind::Pdb => "pdb",
-        }
-    }
-}
-
-impl FromStr for DebuginfoKind {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "dwarf" => DebuginfoKind::Dwarf,
-            "dwarf-dsym" => DebuginfoKind::DwarfDsym,
-            "pdb" => DebuginfoKind::Pdb,
-            _ => {
-                return Err(format!(
-                    "'{s}' is not a valid value for debuginfo-kind. Use 'dwarf', \
-                        'dwarf-dsym' or 'pdb'."
-                ));
-            }
-        })
-    }
-}
-
-crate::json::serde_deserialize_from_str!(DebuginfoKind);
-
-impl ToJson for DebuginfoKind {
-    fn to_json(&self) -> Json {
-        self.as_str().to_json()
-    }
-}
-
-impl fmt::Display for DebuginfoKind {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_str(self.as_str())
-    }
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
-pub enum SplitDebuginfo {
-    /// Split debug-information is disabled, meaning that on supported platforms
-    /// you can find all debug information in the executable itself. This is
-    /// only supported for ELF effectively.
-    ///
-    /// * Windows - not supported
-    /// * macOS - don't run `dsymutil`
-    /// * ELF - `.debug_*` sections
-    #[default]
-    Off,
-
-    /// Split debug-information can be found in a "packed" location separate
-    /// from the final artifact. This is supported on all platforms.
+crate::target_spec_enum! {
+    /// Which kind of debuginfo does the target use?
     ///
-    /// * Windows - `*.pdb`
-    /// * macOS - `*.dSYM` (run `dsymutil`)
-    /// * ELF - `*.dwp` (run `thorin`)
-    Packed,
-
-    /// Split debug-information can be found in individual object files on the
-    /// filesystem. The main executable may point to the object files.
-    ///
-    /// * Windows - not supported
-    /// * macOS - supported, scattered object files
-    /// * ELF - supported, scattered `*.dwo` or `*.o` files (see `SplitDwarfKind`)
-    Unpacked,
-}
-
-impl SplitDebuginfo {
-    fn as_str(&self) -> &'static str {
-        match self {
-            SplitDebuginfo::Off => "off",
-            SplitDebuginfo::Packed => "packed",
-            SplitDebuginfo::Unpacked => "unpacked",
-        }
-    }
-}
-
-impl FromStr for SplitDebuginfo {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "off" => SplitDebuginfo::Off,
-            "unpacked" => SplitDebuginfo::Unpacked,
-            "packed" => SplitDebuginfo::Packed,
-            _ => {
-                return Err(format!(
-                    "'{s}' is not a valid value for \
-                        split-debuginfo. Use 'off', 'unpacked', or 'packed'.",
-                ));
-            }
-        })
-    }
-}
-
-crate::json::serde_deserialize_from_str!(SplitDebuginfo);
-
-impl ToJson for SplitDebuginfo {
-    fn to_json(&self) -> Json {
-        self.as_str().to_json()
-    }
-}
-
-impl fmt::Display for SplitDebuginfo {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_str(self.as_str())
-    }
+    /// Useful in determining whether a target supports Split DWARF (a target with
+    /// `DebuginfoKind::Dwarf` and supporting `SplitDebuginfo::Unpacked` for example).
+    #[derive(Default)]
+    pub enum DebuginfoKind {
+        /// DWARF debuginfo (such as that used on `x86_64_unknown_linux_gnu`).
+        #[default]
+        Dwarf = "dwarf",
+        /// DWARF debuginfo in dSYM files (such as on Apple platforms).
+        DwarfDsym = "dwarf-dsym",
+        /// Program database files (such as on Windows).
+        Pdb = "pdb",
+    }
+
+    parse_error_type = "debuginfo kind";
+}
+
+crate::target_spec_enum! {
+    #[derive(Default)]
+    pub enum SplitDebuginfo {
+        /// Split debug-information is disabled, meaning that on supported platforms
+        /// you can find all debug information in the executable itself. This is
+        /// only supported for ELF effectively.
+        ///
+        /// * Windows - not supported
+        /// * macOS - don't run `dsymutil`
+        /// * ELF - `.debug_*` sections
+        #[default]
+        Off = "off",
+
+        /// Split debug-information can be found in a "packed" location separate
+        /// from the final artifact. This is supported on all platforms.
+        ///
+        /// * Windows - `*.pdb`
+        /// * macOS - `*.dSYM` (run `dsymutil`)
+        /// * ELF - `*.dwp` (run `thorin`)
+        Packed = "packed",
+
+        /// Split debug-information can be found in individual object files on the
+        /// filesystem. The main executable may point to the object files.
+        ///
+        /// * Windows - not supported
+        /// * macOS - supported, scattered object files
+        /// * ELF - supported, scattered `*.dwo` or `*.o` files (see `SplitDwarfKind`)
+        Unpacked = "unpacked",
+    }
+
+    parse_error_type = "split debuginfo";
 }
 
 into_diag_arg_using_display!(SplitDebuginfo);
 
-#[derive(Clone, Debug, PartialEq, Eq, serde_derive::Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, serde_derive::Deserialize, schemars::JsonSchema)]
 #[serde(tag = "kind")]
 #[serde(rename_all = "kebab-case")]
 pub enum StackProbeType {
@@ -1688,6 +1287,19 @@ impl FromStr for SanitizerSet {
 }
 
 crate::json::serde_deserialize_from_str!(SanitizerSet);
+impl schemars::JsonSchema for SanitizerSet {
+    fn schema_name() -> std::borrow::Cow<'static, str> {
+        "SanitizerSet".into()
+    }
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let all = Self::all().iter().map(|sanitizer| sanitizer.as_str()).collect::<Vec<_>>();
+        schemars::json_schema! ({
+            "type": "string",
+            "enum": all,
+        })
+        .into()
+    }
+}
 
 impl ToJson for SanitizerSet {
     fn to_json(&self) -> Json {
@@ -1699,17 +1311,20 @@ impl ToJson for SanitizerSet {
     }
 }
 
-#[derive(Clone, Copy, PartialEq, Hash, Debug)]
-pub enum FramePointer {
-    /// Forces the machine code generator to always preserve the frame pointers.
-    Always,
-    /// Forces the machine code generator to preserve the frame pointers except for the leaf
-    /// functions (i.e. those that don't call other functions).
-    NonLeaf,
-    /// Allows the machine code generator to omit the frame pointers.
-    ///
-    /// This option does not guarantee that the frame pointers will be omitted.
-    MayOmit,
+crate::target_spec_enum! {
+    pub enum FramePointer {
+        /// Forces the machine code generator to always preserve the frame pointers.
+        Always = "always",
+        /// Forces the machine code generator to preserve the frame pointers except for the leaf
+        /// functions (i.e. those that don't call other functions).
+        NonLeaf = "non-leaf",
+        /// Allows the machine code generator to omit the frame pointers.
+        ///
+        /// This option does not guarantee that the frame pointers will be omitted.
+        MayOmit = "may-omit",
+    }
+
+    parse_error_type = "frame pointer";
 }
 
 impl FramePointer {
@@ -1726,93 +1341,43 @@ impl FramePointer {
     }
 }
 
-impl FromStr for FramePointer {
-    type Err = String;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(match s {
-            "always" => Self::Always,
-            "non-leaf" => Self::NonLeaf,
-            "may-omit" => Self::MayOmit,
-            _ => return Err(format!("'{s}' is not a valid value for frame-pointer")),
-        })
-    }
-}
-
-crate::json::serde_deserialize_from_str!(FramePointer);
-
-impl ToJson for FramePointer {
-    fn to_json(&self) -> Json {
-        match *self {
-            Self::Always => "always",
-            Self::NonLeaf => "non-leaf",
-            Self::MayOmit => "may-omit",
-        }
-        .to_json()
-    }
-}
-
-/// Controls use of stack canaries.
-#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
-pub enum StackProtector {
-    /// Disable stack canary generation.
-    None,
-
-    /// On LLVM, mark all generated LLVM functions with the `ssp` attribute (see
-    /// llvm/docs/LangRef.rst). This triggers stack canary generation in
-    /// functions which contain an array of a byte-sized type with more than
-    /// eight elements.
-    Basic,
+crate::target_spec_enum! {
+    /// Controls use of stack canaries.
+    pub enum StackProtector {
+        /// Disable stack canary generation.
+        None = "none",
 
-    /// On LLVM, mark all generated LLVM functions with the `sspstrong`
-    /// attribute (see llvm/docs/LangRef.rst). This triggers stack canary
-    /// generation in functions which either contain an array, or which take
-    /// the address of a local variable.
-    Strong,
+        /// On LLVM, mark all generated LLVM functions with the `ssp` attribute (see
+        /// llvm/docs/LangRef.rst). This triggers stack canary generation in
+        /// functions which contain an array of a byte-sized type with more than
+        /// eight elements.
+        Basic = "basic",
 
-    /// Generate stack canaries in all functions.
-    All,
-}
+        /// On LLVM, mark all generated LLVM functions with the `sspstrong`
+        /// attribute (see llvm/docs/LangRef.rst). This triggers stack canary
+        /// generation in functions which either contain an array, or which take
+        /// the address of a local variable.
+        Strong = "strong",
 
-impl StackProtector {
-    fn as_str(&self) -> &'static str {
-        match self {
-            StackProtector::None => "none",
-            StackProtector::Basic => "basic",
-            StackProtector::Strong => "strong",
-            StackProtector::All => "all",
-        }
+        /// Generate stack canaries in all functions.
+        All = "all",
     }
-}
 
-impl FromStr for StackProtector {
-    type Err = ();
-
-    fn from_str(s: &str) -> Result<StackProtector, ()> {
-        Ok(match s {
-            "none" => StackProtector::None,
-            "basic" => StackProtector::Basic,
-            "strong" => StackProtector::Strong,
-            "all" => StackProtector::All,
-            _ => return Err(()),
-        })
-    }
-}
-
-impl fmt::Display for StackProtector {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_str(self.as_str())
-    }
+    parse_error_type = "stack protector";
 }
 
 into_diag_arg_using_display!(StackProtector);
 
-#[derive(PartialEq, Clone, Debug)]
-pub enum BinaryFormat {
-    Coff,
-    Elf,
-    MachO,
-    Wasm,
-    Xcoff,
+crate::target_spec_enum! {
+    pub enum BinaryFormat {
+        Coff = "coff",
+        Elf = "elf",
+        MachO = "mach-o",
+        Wasm = "wasm",
+        Xcoff = "xcoff",
+    }
+
+    parse_error_type = "binary format";
 }
 
 impl BinaryFormat {
@@ -1828,38 +1393,6 @@ impl BinaryFormat {
     }
 }
 
-impl FromStr for BinaryFormat {
-    type Err = String;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "coff" => Ok(Self::Coff),
-            "elf" => Ok(Self::Elf),
-            "mach-o" => Ok(Self::MachO),
-            "wasm" => Ok(Self::Wasm),
-            "xcoff" => Ok(Self::Xcoff),
-            _ => Err(format!(
-                "'{s}' is not a valid value for binary_format. \
-                    Use 'coff', 'elf', 'mach-o', 'wasm' or 'xcoff' "
-            )),
-        }
-    }
-}
-
-crate::json::serde_deserialize_from_str!(BinaryFormat);
-
-impl ToJson for BinaryFormat {
-    fn to_json(&self) -> Json {
-        match self {
-            Self::Coff => "coff",
-            Self::Elf => "elf",
-            Self::MachO => "mach-o",
-            Self::Wasm => "wasm",
-            Self::Xcoff => "xcoff",
-        }
-        .to_json()
-    }
-}
-
 impl ToJson for Align {
     fn to_json(&self) -> Json {
         self.bits().to_json()
diff --git a/src/bootstrap/src/core/build_steps/dist.rs b/src/bootstrap/src/core/build_steps/dist.rs
index a5f0718bc01..99a1062109a 100644
--- a/src/bootstrap/src/core/build_steps/dist.rs
+++ b/src/bootstrap/src/core/build_steps/dist.rs
@@ -590,6 +590,8 @@ impl Step for Rustc {
             // Debugger scripts
             builder.ensure(DebuggerScripts { sysroot: image.to_owned(), target });
 
+            generate_target_spec_json_schema(builder, image);
+
             // HTML copyright files
             let file_list = builder.ensure(super::run::GenerateCopyright);
             for file in file_list {
@@ -618,6 +620,28 @@ impl Step for Rustc {
     }
 }
 
+fn generate_target_spec_json_schema(builder: &Builder<'_>, sysroot: &Path) {
+    // Since we run rustc in bootstrap, we need to ensure that we use the host compiler.
+    // We do this by using the stage 1 compiler, which is always compiled for the host,
+    // even in a cross build.
+    let stage1_host = builder.compiler(1, builder.host_target);
+    let mut rustc = command(builder.rustc(stage1_host)).fail_fast();
+    rustc
+        .env("RUSTC_BOOTSTRAP", "1")
+        .args(["--print=target-spec-json-schema", "-Zunstable-options"]);
+    let schema = rustc.run_capture(builder).stdout();
+
+    let schema_dir = tmpdir(builder);
+    t!(fs::create_dir_all(&schema_dir));
+    let schema_file = schema_dir.join("target-spec-json-schema.json");
+    t!(std::fs::write(&schema_file, schema));
+
+    let dst = sysroot.join("etc");
+    t!(fs::create_dir_all(&dst));
+
+    builder.install(&schema_file, &dst, FileType::Regular);
+}
+
 /// Copies debugger scripts for `target` into the given compiler `sysroot`.
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct DebuggerScripts {
diff --git a/src/bootstrap/src/utils/proc_macro_deps.rs b/src/bootstrap/src/utils/proc_macro_deps.rs
index 777c8601aa1..db2369097d6 100644
--- a/src/bootstrap/src/utils/proc_macro_deps.rs
+++ b/src/bootstrap/src/utils/proc_macro_deps.rs
@@ -43,6 +43,7 @@ pub static CRATES: &[&str] = &[
     "rustc-hash",
     "self_cell",
     "serde",
+    "serde_derive_internals",
     "sha2",
     "smallvec",
     "stable_deref_trait",
diff --git a/src/doc/rustc/src/targets/custom.md b/src/doc/rustc/src/targets/custom.md
index 6c1494186a4..e1750e27f0b 100644
--- a/src/doc/rustc/src/targets/custom.md
+++ b/src/doc/rustc/src/targets/custom.md
@@ -16,6 +16,21 @@ rustc +nightly -Z unstable-options --target=wasm32-unknown-unknown --print targe
 
 To use a custom target, see the (unstable) [`build-std` feature](../../cargo/reference/unstable.html#build-std) of `cargo`.
 
+<div class="warning">
+
+The target JSON properties are not stable and subject to change.
+Always pin your compiler version when using custom targets!
+
+</div>
+
+## JSON Schema
+
+`rustc` provides a JSON schema for the custom target JSON specification.
+Because the schema is subject to change, you should always use the schema from the version of rustc which you are passing the target to.
+
+It can be found in `etc/target-spec-json-schema.json` in the sysroot (`rustc --print sysroot`) or printed with `rustc +nightly -Zunstable-options --print target-spec-json-schema`.
+The existence and name of this schema is, just like the properties of the JSON specification, not stable and subject to change.
+
 ## Custom Target Lookup Path
 
 When `rustc` is given an option `--target=TARGET` (where `TARGET` is any string), it uses the following logic:
diff --git a/src/tools/tidy/src/deps.rs b/src/tools/tidy/src/deps.rs
index 60347b2ea64..a9804761400 100644
--- a/src/tools/tidy/src/deps.rs
+++ b/src/tools/tidy/src/deps.rs
@@ -267,6 +267,7 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
     "digest",
     "displaydoc",
     "dissimilar",
+    "dyn-clone",
     "either",
     "elsa",
     "ena",
@@ -346,6 +347,8 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
     "rand_xorshift", // dependency for doc-tests in rustc_thread_pool
     "rand_xoshiro",
     "redox_syscall",
+    "ref-cast",
+    "ref-cast-impl",
     "regex",
     "regex-automata",
     "regex-syntax",
@@ -357,11 +360,14 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
     "rustix",
     "ruzstd", // via object in thorin-dwp
     "ryu",
+    "schemars",
+    "schemars_derive",
     "scoped-tls",
     "scopeguard",
     "self_cell",
     "serde",
     "serde_derive",
+    "serde_derive_internals",
     "serde_json",
     "serde_path_to_error",
     "sha1",
diff --git a/tests/run-make/print-request-help-stable-unstable/help-diff.diff b/tests/run-make/print-request-help-stable-unstable/help-diff.diff
index 07eafca3271..044302a19a0 100644
--- a/tests/run-make/print-request-help-stable-unstable/help-diff.diff
+++ b/tests/run-make/print-request-help-stable-unstable/help-diff.diff
@@ -2,6 +2,6 @@
  error: unknown print request: `xxx`
    |
 -  = help: valid print requests are: `calling-conventions`, `cfg`, `code-models`, `crate-name`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `tls-models`
-+  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `tls-models`
++  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `target-spec-json-schema`, `tls-models`
    = help: for more information, see the rustc book: https://doc.rust-lang.org/rustc/command-line-arguments.html#--print-print-compiler-information
  
diff --git a/tests/run-make/print-request-help-stable-unstable/unstable-invalid-print-request-help.err b/tests/run-make/print-request-help-stable-unstable/unstable-invalid-print-request-help.err
index 50ef340e3dd..cc6c3c909b3 100644
--- a/tests/run-make/print-request-help-stable-unstable/unstable-invalid-print-request-help.err
+++ b/tests/run-make/print-request-help-stable-unstable/unstable-invalid-print-request-help.err
@@ -1,5 +1,5 @@
 error: unknown print request: `xxx`
   |
-  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `tls-models`
+  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `target-spec-json-schema`, `tls-models`
   = help: for more information, see the rustc book: https://doc.rust-lang.org/rustc/command-line-arguments.html#--print-print-compiler-information
 
diff --git a/tests/run-make/rustc-help/help-v.stdout b/tests/run-make/rustc-help/help-v.stdout
index 3fc297fb08e..cd161c51ee3 100644
--- a/tests/run-make/rustc-help/help-v.stdout
+++ b/tests/run-make/rustc-help/help-v.stdout
@@ -43,7 +43,7 @@ Options:
         --print <INFO>[=<FILE>]
                         Compiler information to print on stdout (or to a file)
                         INFO may be one of
-                        <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|tls-models>.
+                        <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|target-spec-json-schema|tls-models>.
     -g                  Equivalent to -C debuginfo=2
     -O                  Equivalent to -C opt-level=3
     -o <FILENAME>       Write output to FILENAME
diff --git a/tests/run-make/rustc-help/help.stdout b/tests/run-make/rustc-help/help.stdout
index caffe28f498..74ec083bdee 100644
--- a/tests/run-make/rustc-help/help.stdout
+++ b/tests/run-make/rustc-help/help.stdout
@@ -43,7 +43,7 @@ Options:
         --print <INFO>[=<FILE>]
                         Compiler information to print on stdout (or to a file)
                         INFO may be one of
-                        <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|tls-models>.
+                        <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|target-spec-json-schema|tls-models>.
     -g                  Equivalent to -C debuginfo=2
     -O                  Equivalent to -C opt-level=3
     -o <FILENAME>       Write output to FILENAME
diff --git a/tests/ui/invalid-compile-flags/print-without-arg.stderr b/tests/ui/invalid-compile-flags/print-without-arg.stderr
index 3048a59d0d0..4163d4e0602 100644
--- a/tests/ui/invalid-compile-flags/print-without-arg.stderr
+++ b/tests/ui/invalid-compile-flags/print-without-arg.stderr
@@ -3,5 +3,5 @@ error: Argument to option 'print' missing
            --print <INFO>[=<FILE>]
                                Compiler information to print on stdout (or to a file)
                                INFO may be one of
-                               <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|tls-models>.
+                               <all-target-specs-json|calling-conventions|cfg|check-cfg|code-models|crate-name|crate-root-lint-levels|deployment-target|file-names|host-tuple|link-args|native-static-libs|relocation-models|split-debuginfo|stack-protector-strategies|supported-crate-types|sysroot|target-cpus|target-features|target-libdir|target-list|target-spec-json|target-spec-json-schema|tls-models>.
 
diff --git a/tests/ui/invalid-compile-flags/print.stderr b/tests/ui/invalid-compile-flags/print.stderr
index e3374eb1e6e..e8adbfd87d7 100644
--- a/tests/ui/invalid-compile-flags/print.stderr
+++ b/tests/ui/invalid-compile-flags/print.stderr
@@ -1,5 +1,5 @@
 error: unknown print request: `yyyy`
   |
-  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `tls-models`
+  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `target-spec-json-schema`, `tls-models`
   = help: for more information, see the rustc book: https://doc.rust-lang.org/rustc/command-line-arguments.html#--print-print-compiler-information
 
diff --git a/tests/ui/print-request/print-lints-help.stderr b/tests/ui/print-request/print-lints-help.stderr
index bc48b2fa73c..297a3aa79e1 100644
--- a/tests/ui/print-request/print-lints-help.stderr
+++ b/tests/ui/print-request/print-lints-help.stderr
@@ -1,6 +1,6 @@
 error: unknown print request: `lints`
   |
-  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `tls-models`
+  = help: valid print requests are: `all-target-specs-json`, `calling-conventions`, `cfg`, `check-cfg`, `code-models`, `crate-name`, `crate-root-lint-levels`, `deployment-target`, `file-names`, `host-tuple`, `link-args`, `native-static-libs`, `relocation-models`, `split-debuginfo`, `stack-protector-strategies`, `supported-crate-types`, `sysroot`, `target-cpus`, `target-features`, `target-libdir`, `target-list`, `target-spec-json`, `target-spec-json-schema`, `tls-models`
   = help: use `-Whelp` to print a list of lints
   = help: for more information, see the rustc book: https://doc.rust-lang.org/rustc/command-line-arguments.html#--print-print-compiler-information