diff --git a/Cargo.lock b/Cargo.lock
index ffaedcc63c1de..fc177092815b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2439,6 +2439,7 @@ dependencies = [
  "ruff_text_size",
  "rustc-hash 2.1.0",
  "salsa",
+ "schemars",
  "serde",
  "thiserror 2.0.11",
  "toml",
@@ -2478,6 +2479,7 @@ dependencies = [
  "ruff_text_size",
  "rustc-hash 2.1.0",
  "salsa",
+ "schemars",
  "serde",
  "smallvec",
  "static_assertions",
@@ -2766,6 +2768,7 @@ dependencies = [
  "ruff_text_size",
  "rustc-hash 2.1.0",
  "salsa",
+ "schemars",
  "serde",
  "tempfile",
  "thiserror 2.0.11",
@@ -2790,6 +2793,7 @@ dependencies = [
  "libcst",
  "pretty_assertions",
  "rayon",
+ "red_knot_project",
  "regex",
  "ruff",
  "ruff_diagnostics",
diff --git a/crates/red_knot_project/Cargo.toml b/crates/red_knot_project/Cargo.toml
index 792f1b257ad84..43e1b2d190d8b 100644
--- a/crates/red_knot_project/Cargo.toml
+++ b/crates/red_knot_project/Cargo.toml
@@ -28,6 +28,7 @@ pep440_rs = { workspace = true }
 rayon = { workspace = true }
 rustc-hash = { workspace = true }
 salsa = { workspace = true }
+schemars = { workspace = true, optional = true }
 serde = { workspace = true }
 thiserror = { workspace = true }
 toml = { workspace = true }
@@ -40,8 +41,9 @@ insta = { workspace = true, features = ["redactions", "ron"] }
 
 [features]
 default = ["zstd"]
-zstd = ["red_knot_vendored/zstd"]
 deflate = ["red_knot_vendored/deflate"]
+schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
+zstd = ["red_knot_vendored/zstd"]
 
 [lints]
 workspace = true
diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs
index 13c27837c75f6..21882812211e6 100644
--- a/crates/red_knot_project/src/metadata/options.rs
+++ b/crates/red_knot_project/src/metadata/options.rs
@@ -18,13 +18,16 @@ use thiserror::Error;
 /// The options for the project.
 #[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct Options {
+    /// Configures the type checking environment.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub environment: Option<EnvironmentOptions>,
 
     #[serde(skip_serializing_if = "Option::is_none")]
     pub src: Option<SrcOptions>,
 
+    /// Configures the enabled lints and their severity.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub rules: Option<Rules>,
 }
@@ -177,10 +180,22 @@ impl Options {
 
 #[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct EnvironmentOptions {
+    /// Specifies the version of Python that will be used to execute the source code.
+    /// The version should be specified as a string in the format `M.m` where `M` is the major version
+    /// and `m` is the minor (e.g. "3.0" or "3.6").
+    /// If a version is provided, knot will generate errors if the source code makes use of language features
+    /// that are not supported in that version.
+    /// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub python_version: Option<RangedValue<PythonVersion>>,
 
+    /// Specifies the target platform that will be used to execute the source code.
+    /// If specified, Red Knot will tailor its use of type stub files,
+    /// which conditionalize type definitions based on the platform.
+    ///
+    /// If no platform is specified, knot will use `all` or the current platform in the LSP use case.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub python_platform: Option<RangedValue<PythonPlatform>>,
 
@@ -204,6 +219,7 @@ pub struct EnvironmentOptions {
 
 #[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct SrcOptions {
     /// The root of the project, used for finding first-party modules.
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -212,7 +228,9 @@ pub struct SrcOptions {
 
 #[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", transparent)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct Rules {
+    #[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
     inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
 }
 
@@ -226,6 +244,69 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
     }
 }
 
+#[cfg(feature = "schemars")]
+mod schema {
+    use crate::DEFAULT_LINT_REGISTRY;
+    use red_knot_python_semantic::lint::Level;
+    use schemars::gen::SchemaGenerator;
+    use schemars::schema::{
+        InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
+    };
+    use schemars::JsonSchema;
+
+    pub(super) struct Rules;
+
+    impl JsonSchema for Rules {
+        fn schema_name() -> String {
+            "Rules".to_string()
+        }
+
+        fn json_schema(gen: &mut SchemaGenerator) -> Schema {
+            let registry = &*DEFAULT_LINT_REGISTRY;
+
+            let level_schema = gen.subschema_for::<Level>();
+
+            let properties: schemars::Map<String, Schema> = registry
+                .lints()
+                .iter()
+                .map(|lint| {
+                    (
+                        lint.name().to_string(),
+                        Schema::Object(SchemaObject {
+                            metadata: Some(Box::new(Metadata {
+                                title: Some(lint.summary().to_string()),
+                                description: Some(lint.documentation()),
+                                deprecated: lint.status.is_deprecated(),
+                                default: Some(lint.default_level.to_string().into()),
+                                ..Metadata::default()
+                            })),
+                            subschemas: Some(Box::new(SubschemaValidation {
+                                one_of: Some(vec![level_schema.clone()]),
+                                ..Default::default()
+                            })),
+                            ..Default::default()
+                        }),
+                    )
+                })
+                .collect();
+
+            Schema::Object(SchemaObject {
+                instance_type: Some(InstanceType::Object.into()),
+                object: Some(Box::new(ObjectValidation {
+                    properties,
+                    // Allow unknown rules: Red Knot will warn about them.
+                    // It gives a better experience when using an older Red Knot version because
+                    // the schema will not deny rules that have been removed in newer versions.
+                    additional_properties: Some(Box::new(level_schema)),
+                    ..ObjectValidation::default()
+                })),
+
+                ..Default::default()
+            })
+        }
+    }
+}
+
 #[derive(Error, Debug)]
 pub enum KnotTomlError {
     #[error(transparent)]
diff --git a/crates/red_knot_project/src/metadata/value.rs b/crates/red_knot_project/src/metadata/value.rs
index c4a663ad8fe9f..9e047580f0433 100644
--- a/crates/red_knot_project/src/metadata/value.rs
+++ b/crates/red_knot_project/src/metadata/value.rs
@@ -1,8 +1,9 @@
 use crate::combine::Combine;
 use crate::Db;
 use ruff_db::system::{System, SystemPath, SystemPathBuf};
+use ruff_macros::Combine;
 use ruff_text_size::{TextRange, TextSize};
-use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer};
 use std::cell::RefCell;
 use std::cmp::Ordering;
 use std::fmt;
@@ -70,15 +71,19 @@ impl Drop for ValueSourceGuard {
 ///
 /// This ensures that two resolved configurations are identical even if the position of a value has changed
 /// or if the values were loaded from different sources.
-#[derive(Clone)]
+#[derive(Clone, serde::Serialize)]
+#[serde(transparent)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct RangedValue<T> {
     value: T,
+    #[serde(skip)]
     source: ValueSource,
 
     /// The byte range of `value` in `source`.
     ///
     /// Can be `None` because not all sources support a range.
     /// For example, arguments provided on the CLI won't have a range attached.
+    #[serde(skip)]
     range: Option<TextRange>,
 }
 
@@ -266,18 +271,6 @@ where
     }
 }
 
-impl<T> Serialize for RangedValue<T>
-where
-    T: Serialize,
-{
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        self.value.serialize(serializer)
-    }
-}
-
 /// A possibly relative path in a configuration file.
 ///
 /// Relative paths in configuration files or from CLI options
@@ -286,9 +279,19 @@ where
 /// * CLI: The path is relative to the current working directory
 /// * Configuration file: The path is relative to the project's root.
 #[derive(
-    Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
+    Debug,
+    Clone,
+    serde::Serialize,
+    serde::Deserialize,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Hash,
+    Combine,
 )]
 #[serde(transparent)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
 
 impl RelativePathBuf {
@@ -325,13 +328,3 @@ impl RelativePathBuf {
         SystemPath::absolute(&self.0, relative_to)
     }
 }
-
-impl Combine for RelativePathBuf {
-    fn combine(self, other: Self) -> Self {
-        Self(self.0.combine(other.0))
-    }
-
-    fn combine_with(&mut self, other: Self) {
-        self.0.combine_with(other.0);
-    }
-}
diff --git a/crates/red_knot_python_semantic/Cargo.toml b/crates/red_knot_python_semantic/Cargo.toml
index fb579f614c136..cafd00e9c050b 100644
--- a/crates/red_knot_python_semantic/Cargo.toml
+++ b/crates/red_knot_python_semantic/Cargo.toml
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
 tracing = { workspace = true }
 rustc-hash = { workspace = true }
 hashbrown = { workspace = true }
+schemars = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
 smallvec = { workspace = true }
 static_assertions = { workspace = true }
diff --git a/crates/red_knot_python_semantic/src/lint.rs b/crates/red_knot_python_semantic/src/lint.rs
index 6a8f29f36b71c..0e229536664a4 100644
--- a/crates/red_knot_python_semantic/src/lint.rs
+++ b/crates/red_knot_python_semantic/src/lint.rs
@@ -1,6 +1,8 @@
+use core::fmt;
 use itertools::Itertools;
 use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
 use rustc_hash::FxHashMap;
+use std::fmt::Formatter;
 use std::hash::Hasher;
 use thiserror::Error;
 
@@ -36,13 +38,20 @@ pub struct LintMetadata {
     derive(serde::Serialize, serde::Deserialize),
     serde(rename_all = "kebab-case")
 )]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub enum Level {
+    /// # Ignore
+    ///
     /// The lint is disabled and should not run.
     Ignore,
 
+    /// # Warn
+    ///
     /// The lint is enabled and diagnostic should have a warning severity.
     Warn,
 
+    /// # Error
+    ///
     /// The lint is enabled and diagnostics have an error severity.
     Error,
 }
@@ -61,6 +70,16 @@ impl Level {
     }
 }
 
+impl fmt::Display for Level {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Level::Ignore => f.write_str("ignore"),
+            Level::Warn => f.write_str("warn"),
+            Level::Error => f.write_str("error"),
+        }
+    }
+}
+
 impl TryFrom<Level> for Severity {
     type Error = ();
 
@@ -84,9 +103,11 @@ impl LintMetadata {
 
     /// Returns the documentation line by line with one leading space and all trailing whitespace removed.
     pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
-        self.raw_documentation
-            .lines()
-            .map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
+        self.raw_documentation.lines().map(|line| {
+            line.strip_prefix(char::is_whitespace)
+                .unwrap_or(line)
+                .trim_end()
+        })
     }
 
     /// Returns the documentation as a single string.
@@ -180,6 +201,10 @@ impl LintStatus {
     pub const fn is_removed(&self) -> bool {
         matches!(self, LintStatus::Removed { .. })
     }
+
+    pub const fn is_deprecated(&self) -> bool {
+        matches!(self, LintStatus::Deprecated { .. })
+    }
 }
 
 /// Declares a lint rule with the given metadata.
@@ -223,7 +248,7 @@ macro_rules! declare_lint {
         $vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
             name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
             summary: $summary,
-            raw_documentation: concat!($($doc,)+ "\n"),
+            raw_documentation: concat!($($doc, '\n',)+),
             status: $status,
             file: file!(),
             line: line!(),
diff --git a/crates/red_knot_python_semantic/src/python_platform.rs b/crates/red_knot_python_semantic/src/python_platform.rs
index 9711e433b3052..5a8a8bd3dadbc 100644
--- a/crates/red_knot_python_semantic/src/python_platform.rs
+++ b/crates/red_knot_python_semantic/src/python_platform.rs
@@ -11,6 +11,7 @@ pub enum PythonPlatform {
     /// Do not make any assumptions about the target platform.
     #[default]
     All,
+
     /// Assume a specific target platform like `linux`, `darwin` or `win32`.
     ///
     /// We use a string (instead of individual enum variants), as the set of possible platforms
@@ -28,3 +29,77 @@ impl Display for PythonPlatform {
         }
     }
 }
+
+#[cfg(feature = "schemars")]
+mod schema {
+    use crate::PythonPlatform;
+    use schemars::_serde_json::Value;
+    use schemars::gen::SchemaGenerator;
+    use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
+    use schemars::JsonSchema;
+
+    impl JsonSchema for PythonPlatform {
+        fn schema_name() -> String {
+            "PythonPlatform".to_string()
+        }
+
+        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
+            Schema::Object(SchemaObject {
+                // Hard code some well known values, but allow any other string as well.
+                subschemas: Some(Box::new(SubschemaValidation {
+                    any_of: Some(vec![
+                        Schema::Object(SchemaObject {
+                            instance_type: Some(schemars::schema::InstanceType::String.into()),
+                            ..SchemaObject::default()
+                        }),
+                        // Promote well-known values for better auto-completion.
+                        // Using `const` over `enumValues` as recommended [here](https://github.com/SchemaStore/schemastore/blob/master/CONTRIBUTING.md#documenting-enums).
+                        Schema::Object(SchemaObject {
+                            const_value: Some(Value::String("all".to_string())),
+                            metadata: Some(Box::new(Metadata {
+                                description: Some(
+                                    "Do not make any assumptions about the target platform."
+                                        .to_string(),
+                                ),
+                                ..Metadata::default()
+                            })),
+
+                            ..SchemaObject::default()
+                        }),
+                        Schema::Object(SchemaObject {
+                            const_value: Some(Value::String("darwin".to_string())),
+                            metadata: Some(Box::new(Metadata {
+                                description: Some("Darwin".to_string()),
+                                ..Metadata::default()
+                            })),
+
+                            ..SchemaObject::default()
+                        }),
+                        Schema::Object(SchemaObject {
+                            const_value: Some(Value::String("linux".to_string())),
+                            metadata: Some(Box::new(Metadata {
+                                description: Some("Linux".to_string()),
+                                ..Metadata::default()
+                            })),
+
+                            ..SchemaObject::default()
+                        }),
+                        Schema::Object(SchemaObject {
+                            const_value: Some(Value::String("win32".to_string())),
+                            metadata: Some(Box::new(Metadata {
+                                description: Some("Windows".to_string()),
+                                ..Metadata::default()
+                            })),
+
+                            ..SchemaObject::default()
+                        }),
+                    ]),
+
+                    ..SubschemaValidation::default()
+                })),
+
+                ..SchemaObject::default()
+            })
+        }
+    }
+}
diff --git a/crates/red_knot_python_semantic/src/python_version.rs b/crates/red_knot_python_semantic/src/python_version.rs
index d698cef763081..a161dd1e856ab 100644
--- a/crates/red_knot_python_semantic/src/python_version.rs
+++ b/crates/red_knot_python_semantic/src/python_version.rs
@@ -31,6 +31,20 @@ impl PythonVersion {
         minor: 13,
     };
 
+    pub fn iter() -> impl Iterator<Item = PythonVersion> {
+        [
+            PythonVersion::PY37,
+            PythonVersion::PY38,
+            PythonVersion::PY39,
+            PythonVersion::PY310,
+            PythonVersion::PY311,
+            PythonVersion::PY312,
+            PythonVersion::PY313,
+        ]
+        .iter()
+        .copied()
+    }
+
     pub fn free_threaded_build_available(self) -> bool {
         self >= PythonVersion::PY313
     }
@@ -69,40 +83,86 @@ impl fmt::Display for PythonVersion {
 }
 
 #[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for PythonVersion {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let as_str = String::deserialize(deserializer)?;
-
-        if let Some((major, minor)) = as_str.split_once('.') {
-            let major = major
-                .parse()
-                .map_err(|err| serde::de::Error::custom(format!("invalid major version: {err}")))?;
-            let minor = minor
-                .parse()
-                .map_err(|err| serde::de::Error::custom(format!("invalid minor version: {err}")))?;
-
-            Ok((major, minor).into())
-        } else {
-            let major = as_str.parse().map_err(|err| {
-                serde::de::Error::custom(format!(
-                    "invalid python-version: {err}, expected: `major.minor`"
-                ))
-            })?;
-
-            Ok((major, 0).into())
+mod serde {
+    use crate::PythonVersion;
+
+    impl<'de> serde::Deserialize<'de> for PythonVersion {
+        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where
+            D: serde::Deserializer<'de>,
+        {
+            let as_str = String::deserialize(deserializer)?;
+
+            if let Some((major, minor)) = as_str.split_once('.') {
+                let major = major.parse().map_err(|err| {
+                    serde::de::Error::custom(format!("invalid major version: {err}"))
+                })?;
+                let minor = minor.parse().map_err(|err| {
+                    serde::de::Error::custom(format!("invalid minor version: {err}"))
+                })?;
+
+                Ok((major, minor).into())
+            } else {
+                let major = as_str.parse().map_err(|err| {
+                    serde::de::Error::custom(format!(
+                        "invalid python-version: {err}, expected: `major.minor`"
+                    ))
+                })?;
+
+                Ok((major, 0).into())
+            }
+        }
+    }
+
+    impl serde::Serialize for PythonVersion {
+        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: serde::Serializer,
+        {
+            serializer.serialize_str(&self.to_string())
         }
     }
 }
 
-#[cfg(feature = "serde")]
-impl serde::Serialize for PythonVersion {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        serializer.serialize_str(&self.to_string())
+#[cfg(feature = "schemars")]
+mod schemars {
+    use super::PythonVersion;
+    use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
+    use schemars::JsonSchema;
+    use schemars::_serde_json::Value;
+
+    impl JsonSchema for PythonVersion {
+        fn schema_name() -> String {
+            "PythonVersion".to_string()
+        }
+
+        fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
+            let sub_schemas = std::iter::once(Schema::Object(SchemaObject {
+                instance_type: Some(schemars::schema::InstanceType::String.into()),
+                string: Some(Box::new(schemars::schema::StringValidation {
+                    pattern: Some(r"^\d+\.\d+$".to_string()),
+                    ..Default::default()
+                })),
+                ..Default::default()
+            }))
+            .chain(Self::iter().map(|v| {
+                Schema::Object(SchemaObject {
+                    const_value: Some(Value::String(v.to_string())),
+                    metadata: Some(Box::new(Metadata {
+                        description: Some(format!("Python {v}")),
+                        ..Metadata::default()
+                    })),
+                    ..SchemaObject::default()
+                })
+            }));
+
+            Schema::Object(SchemaObject {
+                subschemas: Some(Box::new(SubschemaValidation {
+                    any_of: Some(sub_schemas.collect()),
+                    ..Default::default()
+                })),
+                ..SchemaObject::default()
+            })
+        }
     }
 }
diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml
index bb2ecf9702d70..b5443ceeb9a04 100644
--- a/crates/ruff_db/Cargo.toml
+++ b/crates/ruff_db/Cargo.toml
@@ -30,6 +30,7 @@ glob = { workspace = true }
 ignore = { workspace = true, optional = true }
 matchit = { workspace = true }
 salsa = { workspace = true }
+schemars = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
 path-slash = { workspace = true }
 thiserror = { workspace = true }
diff --git a/crates/ruff_db/src/system/path.rs b/crates/ruff_db/src/system/path.rs
index a883cad738fc6..4aea0cbe8b641 100644
--- a/crates/ruff_db/src/system/path.rs
+++ b/crates/ruff_db/src/system/path.rs
@@ -471,7 +471,13 @@ impl ToOwned for SystemPath {
 /// The path is guaranteed to be valid UTF-8.
 #[repr(transparent)]
 #[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
-pub struct SystemPathBuf(Utf8PathBuf);
+#[cfg_attr(
+    feature = "serde",
+    derive(serde::Serialize, serde::Deserialize),
+    serde(transparent)
+)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
 
 impl SystemPathBuf {
     pub fn new() -> Self {
@@ -658,27 +664,6 @@ impl ruff_cache::CacheKey for SystemPathBuf {
     }
 }
 
-#[cfg(feature = "serde")]
-impl serde::Serialize for SystemPath {
-    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        self.0.serialize(serializer)
-    }
-}
-
-#[cfg(feature = "serde")]
-impl serde::Serialize for SystemPathBuf {
-    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        self.0.serialize(serializer)
-    }
-}
-
-#[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for SystemPathBuf {
-    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
-        Utf8PathBuf::deserialize(deserializer).map(SystemPathBuf)
-    }
-}
-
 /// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
 #[repr(transparent)]
 pub struct SystemVirtualPath(str);
diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml
index 7443d06361f50..9432476a5aefd 100644
--- a/crates/ruff_dev/Cargo.toml
+++ b/crates/ruff_dev/Cargo.toml
@@ -11,6 +11,7 @@ repository = { workspace = true }
 license = { workspace = true }
 
 [dependencies]
+red_knot_project = { workspace = true, features = ["schemars"] }
 ruff = { workspace = true }
 ruff_diagnostics = { workspace = true }
 ruff_formatter = { workspace = true }
diff --git a/crates/ruff_dev/src/generate_all.rs b/crates/ruff_dev/src/generate_all.rs
index 6b8212ce6419f..f22c562ae6ef4 100644
--- a/crates/ruff_dev/src/generate_all.rs
+++ b/crates/ruff_dev/src/generate_all.rs
@@ -2,7 +2,7 @@
 
 use anyhow::Result;
 
-use crate::{generate_cli_help, generate_docs, generate_json_schema};
+use crate::{generate_cli_help, generate_docs, generate_json_schema, generate_knot_schema};
 
 pub(crate) const REGENERATE_ALL_COMMAND: &str = "cargo dev generate-all";
 
@@ -33,6 +33,7 @@ impl Mode {
 
 pub(crate) fn main(args: &Args) -> Result<()> {
     generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
+    generate_knot_schema::main(&generate_knot_schema::Args { mode: args.mode })?;
     generate_cli_help::main(&generate_cli_help::Args { mode: args.mode })?;
     generate_docs::main(&generate_docs::Args {
         dry_run: args.mode.is_dry_run(),
diff --git a/crates/ruff_dev/src/generate_knot_schema.rs b/crates/ruff_dev/src/generate_knot_schema.rs
new file mode 100644
index 0000000000000..0c6f8320981cc
--- /dev/null
+++ b/crates/ruff_dev/src/generate_knot_schema.rs
@@ -0,0 +1,72 @@
+#![allow(clippy::print_stdout, clippy::print_stderr)]
+
+use std::fs;
+use std::path::PathBuf;
+
+use anyhow::{bail, Result};
+use pretty_assertions::StrComparison;
+use schemars::schema_for;
+
+use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
+use crate::ROOT_DIR;
+use red_knot_project::metadata::options::Options;
+
+#[derive(clap::Args)]
+pub(crate) struct Args {
+    /// Write the generated table to stdout (rather than to `knot.schema.json`).
+    #[arg(long, default_value_t, value_enum)]
+    pub(crate) mode: Mode,
+}
+
+pub(crate) fn main(args: &Args) -> Result<()> {
+    let schema = schema_for!(Options);
+    let schema_string = serde_json::to_string_pretty(&schema).unwrap();
+    let filename = "knot.schema.json";
+    let schema_path = PathBuf::from(ROOT_DIR).join(filename);
+
+    match args.mode {
+        Mode::DryRun => {
+            println!("{schema_string}");
+        }
+        Mode::Check => {
+            let current = fs::read_to_string(schema_path)?;
+            if current == schema_string {
+                println!("Up-to-date: {filename}");
+            } else {
+                let comparison = StrComparison::new(&current, &schema_string);
+                bail!("{filename} changed, please run `{REGENERATE_ALL_COMMAND}`:\n{comparison}");
+            }
+        }
+        Mode::Write => {
+            let current = fs::read_to_string(&schema_path)?;
+            if current == schema_string {
+                println!("Up-to-date: {filename}");
+            } else {
+                println!("Updating: {filename}");
+                fs::write(schema_path, schema_string.as_bytes())?;
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use anyhow::Result;
+    use std::env;
+
+    use crate::generate_all::Mode;
+
+    use super::{main, Args};
+
+    #[test]
+    fn test_generate_json_schema() -> Result<()> {
+        let mode = if env::var("KNOT_UPDATE_SCHEMA").as_deref() == Ok("1") {
+            Mode::Write
+        } else {
+            Mode::Check
+        };
+        main(&Args { mode })
+    }
+}
diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs
index 83bf95f490289..5fb10476da47b 100644
--- a/crates/ruff_dev/src/main.rs
+++ b/crates/ruff_dev/src/main.rs
@@ -13,6 +13,7 @@ mod generate_all;
 mod generate_cli_help;
 mod generate_docs;
 mod generate_json_schema;
+mod generate_knot_schema;
 mod generate_options;
 mod generate_rules_table;
 mod print_ast;
@@ -39,6 +40,8 @@ enum Command {
     GenerateAll(generate_all::Args),
     /// Generate JSON schema for the TOML configuration file.
     GenerateJSONSchema(generate_json_schema::Args),
+    /// Generate JSON schema for the Red Knot TOML configuration file.
+    GenerateKnotSchema(generate_knot_schema::Args),
     /// Generate a Markdown-compatible table of supported lint rules.
     GenerateRulesTable,
     /// Generate a Markdown-compatible listing of configuration options.
@@ -83,6 +86,7 @@ fn main() -> Result<ExitCode> {
     match command {
         Command::GenerateAll(args) => generate_all::main(&args)?,
         Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
+        Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?,
         Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()),
         Command::GenerateOptions => println!("{}", generate_options::generate()),
         Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?,
diff --git a/crates/ruff_macros/src/combine.rs b/crates/ruff_macros/src/combine.rs
index 01b3635272fa9..3d755d8ac09d9 100644
--- a/crates/ruff_macros/src/combine.rs
+++ b/crates/ruff_macros/src/combine.rs
@@ -1,25 +1,18 @@
 use quote::{quote, quote_spanned};
-use syn::{Data, DataStruct, DeriveInput, Fields};
+use syn::spanned::Spanned;
+use syn::{Data, DataStruct, DeriveInput};
 
 pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
     let DeriveInput { ident, data, .. } = input;
 
     match data {
-        Data::Struct(DataStruct {
-            fields: Fields::Named(fields),
-            ..
-        }) => {
+        Data::Struct(DataStruct { fields, .. }) => {
             let output: Vec<_> = fields
-                .named
-                .iter()
-                .map(|field| {
-                    let ident = field
-                        .ident
-                        .as_ref()
-                        .expect("Expected to handle named fields");
+                .members()
+                .map(|member| {
 
                     quote_spanned!(
-                        ident.span() => crate::combine::Combine::combine_with(&mut self.#ident, other.#ident)
+                        member.span() => crate::combine::Combine::combine_with(&mut self.#member, other.#member)
                     )
                 })
                 .collect();
@@ -37,7 +30,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
         }
         _ => Err(syn::Error::new(
             ident.span(),
-            "Can only derive Combine from structs with named fields.",
+            "Can only derive Combine from structs.",
         )),
     }
 }
diff --git a/crates/ruff_macros/src/lib.rs b/crates/ruff_macros/src/lib.rs
index cbe6dc610413e..05913ddee843b 100644
--- a/crates/ruff_macros/src/lib.rs
+++ b/crates/ruff_macros/src/lib.rs
@@ -39,7 +39,7 @@ pub fn derive_combine_options(input: TokenStream) -> TokenStream {
 /// Automatically derives a `red_knot_project::project::Combine` implementation for the attributed type
 /// that calls `red_knot_project::project::Combine::combine` for each field.
 ///
-/// The derive macro can only be used on structs with named fields.
+/// The derive macro can only be used on structs. Enums aren't yet supported.
 #[proc_macro_derive(Combine)]
 pub fn derive_combine(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
diff --git a/knot.schema.json b/knot.schema.json
new file mode 100644
index 0000000000000..f961e34643e3f
--- /dev/null
+++ b/knot.schema.json
@@ -0,0 +1,693 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Options",
+  "description": "The options for the project.",
+  "type": "object",
+  "properties": {
+    "environment": {
+      "description": "Configures the type checking environment.",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/EnvironmentOptions"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
+    "rules": {
+      "description": "Configures the enabled lints and their severity.",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Rules"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
+    "src": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/SrcOptions"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    }
+  },
+  "additionalProperties": false,
+  "definitions": {
+    "EnvironmentOptions": {
+      "type": "object",
+      "properties": {
+        "extra-paths": {
+          "description": "List of user-provided paths that should take first priority in the module resolution. Examples in other type checkers are mypy's MYPYPATH environment variable, or pyright's stubPath configuration setting.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "python-platform": {
+          "description": "Specifies the target platform that will be used to execute the source code. If specified, Red Knot will tailor its use of type stub files, which conditionalize type definitions based on the platform.\n\nIf no platform is specified, knot will use `all` or the current platform in the LSP use case.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PythonPlatform"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "python-version": {
+          "description": "Specifies the version of Python that will be used to execute the source code. The version should be specified as a string in the format `M.m` where `M` is the major version and `m` is the minor (e.g. \"3.0\" or \"3.6\"). If a version is provided, knot will generate errors if the source code makes use of language features that are not supported in that version. It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/PythonVersion"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "typeshed": {
+          "description": "Optional path to a \"typeshed\" directory on disk for us to use for standard-library types. If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, bundled as a zip file in the binary",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "venv-path": {
+          "description": "The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "Level": {
+      "oneOf": [
+        {
+          "title": "Ignore",
+          "description": "The lint is disabled and should not run.",
+          "type": "string",
+          "enum": [
+            "ignore"
+          ]
+        },
+        {
+          "title": "Warn",
+          "description": "The lint is enabled and diagnostic should have a warning severity.",
+          "type": "string",
+          "enum": [
+            "warn"
+          ]
+        },
+        {
+          "title": "Error",
+          "description": "The lint is enabled and diagnostics have an error severity.",
+          "type": "string",
+          "enum": [
+            "error"
+          ]
+        }
+      ]
+    },
+    "PythonPlatform": {
+      "anyOf": [
+        {
+          "type": "string"
+        },
+        {
+          "description": "Do not make any assumptions about the target platform.",
+          "const": "all"
+        },
+        {
+          "description": "Darwin",
+          "const": "darwin"
+        },
+        {
+          "description": "Linux",
+          "const": "linux"
+        },
+        {
+          "description": "Windows",
+          "const": "win32"
+        }
+      ]
+    },
+    "PythonVersion": {
+      "anyOf": [
+        {
+          "type": "string",
+          "pattern": "^\\d+\\.\\d+$"
+        },
+        {
+          "description": "Python 3.7",
+          "const": "3.7"
+        },
+        {
+          "description": "Python 3.8",
+          "const": "3.8"
+        },
+        {
+          "description": "Python 3.9",
+          "const": "3.9"
+        },
+        {
+          "description": "Python 3.10",
+          "const": "3.10"
+        },
+        {
+          "description": "Python 3.11",
+          "const": "3.11"
+        },
+        {
+          "description": "Python 3.12",
+          "const": "3.12"
+        },
+        {
+          "description": "Python 3.13",
+          "const": "3.13"
+        }
+      ]
+    },
+    "Rules": {
+      "type": "object",
+      "properties": {
+        "byte-string-type-annotation": {
+          "title": "detects byte strings in type annotation positions",
+          "description": "## What it does\nChecks for byte-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use byte-string notation.\n\n## Examples\n```python\ndef test(): -> b\"int\":\n    ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n    ...\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "call-non-callable": {
+          "title": "detects calls to non-callable objects",
+          "description": "## What it does\nChecks for calls to non-callable objects.\n\n## Why is this bad?\nCalling a non-callable object will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4()  # TypeError: 'int' object is not callable\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "call-possibly-unbound-method": {
+          "title": "detects calls to possibly unbound methods",
+          "description": "## What it does\nChecks for calls to possibly unbound methods.\n\nTODO #14889",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "conflicting-declarations": {
+          "title": "detects conflicting declarations",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "conflicting-metaclass": {
+          "title": "detects conflicting metaclasses",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "cyclic-class-definition": {
+          "title": "detects cyclic class definitions",
+          "description": "## What it does\nChecks for class definitions with a cyclic inheritance chain.\n\n## Why is it bad?\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "division-by-zero": {
+          "title": "detects division by zero",
+          "description": "## What it does\nIt detects division by zero.\n\n## Why is this bad?\nDividing by zero raises a `ZeroDivisionError` at runtime.\n\n## Examples\n```python\n5 / 0\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "duplicate-base": {
+          "title": "detects class definitions with duplicate bases",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "escape-character-in-forward-annotation": {
+          "title": "detects forward type annotations with escape characters",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "fstring-type-annotation": {
+          "title": "detects F-strings in type annotation positions",
+          "description": "## What it does\nChecks for f-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use f-string notation.\n\n## Examples\n```python\ndef test(): -> f\"int\":\n    ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n    ...\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "implicit-concatenated-string-type-annotation": {
+          "title": "detects implicit concatenated strings in type annotations",
+          "description": "## What it does\nChecks for implicit concatenated strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.\n\n## Examples\n```python\ndef test(): -> \"Literal[\" \"5\" \"]\":\n    ...\n```\n\nUse instead:\n```python\ndef test(): -> \"Literal[5]\":\n    ...\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "incompatible-slots": {
+          "title": "detects class definitions whose MRO has conflicting `__slots__`",
+          "description": "## What it does\nChecks for classes whose bases define incompatible `__slots__`.\n\n## Why is this bad?\nInheriting from bases with incompatible `__slots__`s\nwill lead to a `TypeError` at runtime.\n\nClasses with no or empty `__slots__` are always compatible:\n\n```python\nclass A: ...\nclass B:\n    __slots__ = ()\nclass C:\n    __slots__ = (\"a\", \"b\")\n\n# fine\nclass D(A, B, C): ...\n```\n\nMultiple inheritance from more than one different class\ndefining non-empty `__slots__` is not allowed:\n\n```python\nclass A:\n    __slots__ = (\"a\", \"b\")\n\nclass B:\n    __slots__ = (\"a\", \"b\")  # Even if the values are the same\n\n# TypeError: multiple bases have instance lay-out conflict\nclass C(A, B): ...\n```\n\n## Known problems\nDynamic (not tuple or string literal) `__slots__` are not checked.\nAdditionally, classes inheriting from built-in classes with implicit layouts\nlike `str` or `int` are also not checked.\n\n```pycon\n>>> hasattr(int, \"__slots__\")\nFalse\n>>> hasattr(str, \"__slots__\")\nFalse\n>>> class A(int, str): ...\nTraceback (most recent call last):\n  File \"<python-input-0>\", line 1, in <module>\n    class A(int, str): ...\nTypeError: multiple bases have instance lay-out conflict\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "inconsistent-mro": {
+          "title": "detects class definitions with an inconsistent MRO",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "index-out-of-bounds": {
+          "title": "detects index out of bounds errors",
+          "description": "## What it does\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-argument-type": {
+          "title": "detects call arguments whose type is not assignable to the corresponding typed parameter",
+          "description": "## What it does\nDetects call arguments whose type is not assignable to the corresponding typed parameter.\n\n## Why is this bad?\nPassing an argument of a type the function (or callable object) does not accept violates\nthe expectations of the function author and may cause unexpected runtime errors within the\nbody of the function.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc(\"foo\")  # error: [invalid-argument-type]\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-assignment": {
+          "title": "detects invalid assignments",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-attribute-access": {
+          "title": "Invalid attribute access",
+          "description": "## What it does\nMakes sure that instance attribute accesses are valid.\n\n## Examples\n```python\nclass C:\n  var: ClassVar[int] = 1\n\nC.var = 3  # okay\nC().var = 3  # error: Cannot assign to class variable\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-base": {
+          "title": "detects class definitions with an invalid base",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-context-manager": {
+          "title": "detects expressions used in with statements that don't implement the context manager protocol",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-declaration": {
+          "title": "detects invalid declarations",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-exception-caught": {
+          "title": "detects exception handlers that catch classes that do not inherit from `BaseException`",
+          "description": "## What it does\nChecks for exception handlers that catch non-exception classes.\n\n## Why is this bad?\nCatching classes that do not inherit from `BaseException` will raise a TypeError at runtime.\n\n## Example\n```python\ntry:\n    1 / 0\nexcept 1:\n    ...\n```\n\nUse instead:\n```python\ntry:\n    1 / 0\nexcept ZeroDivisionError:\n    ...\n```\n\n## References\n- [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)\n\n## Ruff rule\n This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-ignore-comment": {
+          "title": "detects ignore comments that use invalid syntax",
+          "description": "## What it does\nChecks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0  # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0  # type: ignore\n```",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-metaclass": {
+          "title": "detects invalid `metaclass=` arguments",
+          "description": "## What it does\nChecks for arguments to `metaclass=` that are invalid.\n\n## Why is this bad?\nPython allows arbitrary expressions to be used as the argument to `metaclass=`.\nThese expressions, however, need to be callable and accept the same arguments\nas `type.__new__`.\n\n## Example\n\n```python\ndef f(): ...\n\n# TypeError: f() takes 0 positional arguments but 3 were given\nclass B(metaclass=f): ...\n```\n\n## References\n- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-parameter-default": {
+          "title": "detects default values that can't be assigned to the parameter's annotated type",
+          "description": "## What it does\nChecks for default values that can't be assigned to the parameter's annotated type.\n\n## Why is this bad?\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-raise": {
+          "title": "detects `raise` statements that raise invalid exceptions or use invalid causes",
+          "description": "Checks for `raise` statements that raise non-exceptions or use invalid\ncauses for their raised exceptions.\n\n## Why is this bad?\nOnly subclasses or instances of `BaseException` can be raised.\nFor an exception's cause, the same rules apply, except that `None` is also\npermitted. Violating these rules results in a `TypeError` at runtime.\n\n## Examples\n```python\ndef f():\n    try:\n        something()\n    except NameError:\n        raise \"oops!\" from f\n\ndef g():\n    raise NotImplemented from 42\n```\n\nUse instead:\n```python\ndef f():\n    try:\n        something()\n    except NameError as e:\n        raise RuntimeError(\"oops!\") from e\n\ndef g():\n    raise NotImplementedError from None\n```\n\n## References\n- [Python documentation: The `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#raise)\n- [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-syntax-in-forward-annotation": {
+          "title": "detects invalid syntax in forward annotations",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-type-form": {
+          "title": "detects invalid type forms",
+          "description": "## What it does\nChecks for invalid type expressions.\n\n## Why is this bad?\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "invalid-type-variable-constraints": {
+          "title": "detects invalid type variable constraints",
+          "description": "TODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "missing-argument": {
+          "title": "detects missing required arguments in a call",
+          "description": "## What it does\nChecks for missing required arguments in a call.\n\n## Why is this bad?\nFailing to provide a required argument will raise a `TypeError` at runtime.\n\n## Examples\n```python\ndef func(x: int): ...\nfunc()  # TypeError: func() missing 1 required positional argument: 'x'\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "non-subscriptable": {
+          "title": "detects subscripting objects that do not support subscripting",
+          "description": "## What it does\nChecks for subscripting objects that do not support subscripting.\n\n## Why is this bad?\nSubscripting an object that does not support it will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4[1]  # TypeError: 'int' object is not subscriptable\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "not-iterable": {
+          "title": "detects iteration over an object that is not iterable",
+          "description": "## What it does\nChecks for objects that are not iterable but are used in a context that requires them to be.\n\n## Why is this bad?\nIterating over an object that is not iterable will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\nfor i in 34:  # TypeError: 'int' object is not iterable\n    pass\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "parameter-already-assigned": {
+          "title": "detects multiple arguments for the same parameter",
+          "description": "## What it does\nChecks for calls which provide more than one argument for a single parameter.\n\n## Why is this bad?\nProviding multiple values for a single parameter will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\ndef f(x: int) -> int: ...\n\nf(1, x=2)  # Error raised here\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "possibly-unbound-attribute": {
+          "title": "detects references to possibly unbound attributes",
+          "description": "## What it does\nChecks for possibly unbound attributes.\n\nTODO #14889",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "possibly-unbound-import": {
+          "title": "detects possibly unbound imports",
+          "description": "TODO #14889",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "possibly-unresolved-reference": {
+          "title": "detects references to possibly undefined names",
+          "description": "## What it does\nChecks for references to names that are possibly not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nfor i in range(0):\n    x = i\n\nprint(x)  # NameError: name 'x' is not defined\n```",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "raw-string-type-annotation": {
+          "title": "detects raw strings in type annotation positions",
+          "description": "## What it does\nChecks for raw-strings in type annotation positions.\n\n## Why is this bad?\nStatic analysis tools like Red Knot can't analyse type annotations that use raw-string notation.\n\n## Examples\n```python\ndef test(): -> r\"int\":\n    ...\n```\n\nUse instead:\n```python\ndef test(): -> \"int\":\n    ...\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "static-assert-error": {
+          "title": "Failed static assertion",
+          "description": "## What it does\nMakes sure that the argument of `static_assert` is statically known to be true.\n\n## Examples\n```python\nfrom knot_extensions import static_assert\n\nstatic_assert(1 + 1 == 3)  # error: evaluates to `False`\n\nstatic_assert(int(2.0 * 3.0) == 6)  # error: does not have a statically known truthiness\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "subclass-of-final-class": {
+          "title": "detects subclasses of final classes",
+          "description": "## What it does\nChecks for classes that subclass final classes.\n\n## Why is this bad?\nDecorating a class with `@final` declares to the type checker that it should not be subclassed.\n\n## Example\n\n```python\nfrom typing import final\n\n@final\nclass A: ...\nclass B(A): ...  # Error raised here\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "too-many-positional-arguments": {
+          "title": "detects calls passing too many positional arguments",
+          "description": "## What it does\nChecks for calls that pass more positional arguments than the callable can accept.\n\n## Why is this bad?\nPassing too many positional arguments will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(): ...\n\nf(\"foo\")  # Error raised here\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "type-assertion-failure": {
+          "title": "detects failed type assertions",
+          "description": "## What it does\nChecks for `assert_type()` calls where the actual type\nis not the same as the asserted type.\n\n## Why is this bad?\n`assert_type()` allows confirming the inferred type of a certain value.\n\n## Example\n\n```python\ndef _(x: int):\n    assert_type(x, int)  # fine\n    assert_type(x, str)  # error: Actual type does not match asserted type\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "undefined-reveal": {
+          "title": "detects usages of `reveal_type` without importing it",
+          "description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\nTODO #14889",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unknown-argument": {
+          "title": "detects unknown keyword arguments in calls",
+          "description": "## What it does\nChecks for keyword arguments in calls that don't match any parameter of the callable.\n\n## Why is this bad?\nProviding an unknown keyword argument will raise `TypeError` at runtime.\n\n## Example\n\n```python\ndef f(x: int) -> int: ...\n\nf(x=1, y=2)  # Error raised here\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unknown-rule": {
+          "title": "detects `knot: ignore` comments that reference unknown rules",
+          "description": "## What it does\nChecks for `knot: ignore[code]` where `code` isn't a known lint rule.\n\n## Why is this bad?\nA `knot: ignore[code]` directive with a `code` that doesn't match\nany known rule will not suppress any type errors, and is probably a mistake.\n\n## Examples\n```py\na = 20 / 0  # knot: ignore[division-by-zer]\n```\n\nUse instead:\n\n```py\na = 20 / 0  # knot: ignore[division-by-zero]\n```",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unresolved-attribute": {
+          "title": "detects references to unresolved attributes",
+          "description": "## What it does\nChecks for unresolved attributes.\n\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unresolved-import": {
+          "title": "detects unresolved imports",
+          "description": "## What it does\nChecks for import statements for which the module cannot be resolved.\n\n## Why is this bad?\nImporting a module that cannot be resolved will raise an `ImportError` at runtime.",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unresolved-reference": {
+          "title": "detects references to names that are not defined",
+          "description": "## What it does\nChecks for references to names that are not defined.\n\n## Why is this bad?\nUsing an undefined variable will raise a `NameError` at runtime.\n\n## Example\n\n```python\nprint(x)  # NameError: name 'x' is not defined\n```",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unsupported-operator": {
+          "title": "detects binary, unary, or comparison expressions where the operands don't support the operator",
+          "description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.\n\nTODO #14889",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "unused-ignore-comment": {
+          "title": "detects unused `type: ignore` comments",
+          "description": "## What it does\nChecks for `type: ignore` or `knot: ignore` directives that are no longer applicable.\n\n## Why is this bad?\nA `type: ignore` directive that no longer matches any diagnostic violations is likely\nincluded by mistake, and should be removed to avoid confusion.\n\n## Examples\n```py\na = 20 / 2  # knot: ignore[division-by-zero]\n```\n\nUse instead:\n\n```py\na = 20 / 2\n```",
+          "default": "warn",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        },
+        "zero-stepsize-in-slice": {
+          "title": "detects a slice step size of zero",
+          "description": "## What it does\nChecks for step size 0 in slices.\n\n## Why is this bad?\nA slice with a step size of zero will raise a `ValueError` at runtime.\n\n## Examples\n```python\nl = list(range(10))\nl[1:10:0]  # ValueError: slice step cannot be zero\n```",
+          "default": "error",
+          "oneOf": [
+            {
+              "$ref": "#/definitions/Level"
+            }
+          ]
+        }
+      },
+      "additionalProperties": {
+        "$ref": "#/definitions/Level"
+      }
+    },
+    "SrcOptions": {
+      "type": "object",
+      "properties": {
+        "root": {
+          "description": "The root of the project, used for finding first-party modules.",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "additionalProperties": false
+    }
+  }
+}
\ No newline at end of file