Detailed changes
@@ -212,7 +212,6 @@ impl AgentSettingsContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
-#[schemars(deny_unknown_fields)]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -12,7 +12,6 @@ pub struct CallSettings {
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.
@@ -378,7 +378,6 @@ pub enum SnippetSortOrder {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -143,7 +143,8 @@ impl JsonSchema for FontFeatures {
"minimum": 0,
"multipleOf": 1
}
- }
+ },
+ "additionalProperties": false
})
}
}
@@ -410,7 +410,6 @@ fn default_lsp_fetch_timeout_ms() -> u64 {
/// The settings for a particular language.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
@@ -269,7 +269,7 @@ impl JsonLspAdapter {
#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json_lenient::Value {
- let schema = schemars::generate::SchemaSettings::draft07()
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
.root_schema_for::<gpui::StyleRefinement>();
@@ -36,7 +36,6 @@ use crate::{
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct ProjectSettings {
/// Configuration for language servers.
///
@@ -427,7 +427,7 @@ impl KeymapFile {
}
pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
- let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
let action_schemas = cx.action_schemas(&mut generator);
let deprecations = cx.deprecated_actions_to_preferred_actions();
@@ -1,17 +1,19 @@
use anyhow::Result;
use gpui::App;
-use schemars::{JsonSchema, Schema};
+use schemars::{JsonSchema, Schema, transform::transform_subschemas};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, StreamingIterator as _};
use util::RangeExt;
+/// Parameters that are used when generating some JSON schemas at runtime.
pub struct SettingsJsonSchemaParams<'a> {
pub language_names: &'a [String],
pub font_names: &'a [String],
}
+/// Value registered which specifies JSON schemas that are generated at runtime.
pub struct ParameterizedJsonSchema {
pub add_and_get_ref:
fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
@@ -19,24 +21,26 @@ pub struct ParameterizedJsonSchema {
inventory::collect!(ParameterizedJsonSchema);
+const DEFS_PATH: &str = "#/$defs/";
+
+/// Replaces the JSON schema definition for some type, and returns a reference to it.
pub fn replace_subschema<T: JsonSchema>(
generator: &mut schemars::SchemaGenerator,
schema: schemars::Schema,
) -> schemars::Schema {
- const DEFINITIONS_PATH: &str = "#/definitions/";
// The key in definitions may not match T::schema_name() if multiple types have the same name.
// This is a workaround for there being no straightforward way to get the key used for a type -
// see https://github.com/GREsau/schemars/issues/449
let ref_schema = generator.subschema_for::<T>();
if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") {
- if let Some(definition_name) = definition_pointer.strip_prefix(DEFINITIONS_PATH) {
+ if let Some(definition_name) = definition_pointer.strip_prefix(DEFS_PATH) {
generator
.definitions_mut()
.insert(definition_name.to_string(), schema.to_value());
return ref_schema;
} else {
log::error!(
- "bug: expected `$ref` field to start with {DEFINITIONS_PATH}, \
+ "bug: expected `$ref` field to start with {DEFS_PATH}, \
got {definition_pointer}"
);
}
@@ -48,7 +52,39 @@ pub fn replace_subschema<T: JsonSchema>(
generator
.definitions_mut()
.insert(schema_name.to_string(), schema.to_value());
- Schema::new_ref(format!("{DEFINITIONS_PATH}{schema_name}"))
+ Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
+}
+
+/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
+/// already in use.
+pub fn add_new_subschema(
+ generator: &mut schemars::SchemaGenerator,
+ name: &str,
+ schema: Value,
+) -> Schema {
+ let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
+ assert_eq!(old_definition, None);
+ schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
+}
+
+/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
+/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
+/// on a map).
+#[derive(Clone)]
+pub struct DefaultDenyUnknownFields;
+
+impl schemars::transform::Transform for DefaultDenyUnknownFields {
+ fn transform(&mut self, schema: &mut schemars::Schema) {
+ if let Some(object) = schema.as_object_mut() {
+ if object.contains_key("properties")
+ && !object.contains_key("additionalProperties")
+ && !object.contains_key("unevaluatedProperties")
+ {
+ object.insert("additionalProperties".to_string(), false.into());
+ }
+ }
+ transform_subschemas(self, schema);
+ }
}
pub fn update_value_in_json_text<'a>(
@@ -24,8 +24,8 @@ use util::{ResultExt as _, merge_non_null_json_value_into};
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{
- ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
- parse_json_with_comments, update_value_in_json_text,
+ DefaultDenyUnknownFields, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
+ WorktreeId, add_new_subschema, parse_json_with_comments, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
@@ -864,7 +864,9 @@ impl SettingsStore {
}
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value {
- let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09()
+ .with_transform(DefaultDenyUnknownFields)
+ .into_generator();
let mut combined_schema = json!({
"type": "object",
"properties": {}
@@ -988,16 +990,34 @@ impl SettingsStore {
}
}
+ // add schemas which are determined at runtime
for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
(parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
}
+ // add merged settings schema to the definitions
const ZED_SETTINGS: &str = "ZedSettings";
- let old_zed_settings_definition = generator
- .definitions_mut()
- .insert(ZED_SETTINGS.to_string(), combined_schema);
- assert_eq!(old_zed_settings_definition, None);
- let zed_settings_ref = schemars::Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}"));
+ let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
+
+ // add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
+ // fields are rejected.
+ let mut zed_release_stage_settings = zed_settings_ref.clone();
+ zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
+ let zed_release_stage_settings_ref = add_new_subschema(
+ &mut generator,
+ "ZedReleaseStageSettings",
+ zed_release_stage_settings.to_value(),
+ );
+
+ // Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
+ // unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
+ let mut definitions = generator.take_definitions(true);
+ definitions
+ .get_mut(ZED_SETTINGS)
+ .unwrap()
+ .as_object_mut()
+ .unwrap()
+ .remove("additionalProperties");
let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() {
json_schema!({ "$schema": meta_schema.to_string() })
@@ -1005,25 +1025,26 @@ impl SettingsStore {
json_schema!({})
};
- // settings file contents matches ZedSettings + overrides for each release stage
+ // "unevaluatedProperties: false" to report unknown fields.
+ root_schema.insert("unevaluatedProperties".to_string(), false.into());
+
+ // Settings file contents matches ZedSettings + overrides for each release stage.
root_schema.insert(
"allOf".to_string(),
json!([
zed_settings_ref,
{
"properties": {
- "dev": zed_settings_ref,
- "nightly": zed_settings_ref,
- "stable": zed_settings_ref,
- "preview": zed_settings_ref,
+ "dev": zed_release_stage_settings_ref,
+ "nightly": zed_release_stage_settings_ref,
+ "stable": zed_release_stage_settings_ref,
+ "preview": zed_release_stage_settings_ref,
}
}
]),
);
- root_schema.insert(
- "definitions".to_string(),
- generator.take_definitions(true).into(),
- );
+
+ root_schema.insert("$defs".to_string(), definitions.into());
root_schema.to_value()
}
@@ -1934,7 +1955,6 @@ mod tests {
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct UserSettingsContent {
name: Option<String>,
age: Option<u32>,
@@ -1977,7 +1997,6 @@ mod tests {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct MultiKeySettingsJson {
key1: Option<String>,
key2: Option<String>,
@@ -2016,7 +2035,6 @@ mod tests {
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct JournalSettingsJson {
pub path: Option<String>,
pub hour_format: Option<HourFormat>,
@@ -2111,7 +2129,6 @@ mod tests {
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct LanguageSettingEntry {
language_setting_1: Option<bool>,
language_setting_2: Option<bool>,
@@ -12,7 +12,7 @@ pub struct VsSnippetsFile {
impl VsSnippetsFile {
pub fn generate_json_schema() -> Value {
- let schema = schemars::generate::SchemaSettings::draft07()
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
.root_schema_for::<Self>();
@@ -287,7 +287,7 @@ pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile {
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
- let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
let build_task_schema = generator.root_schema_for::<BuildTaskDefinition>();
let mut build_task_value =
serde_json_lenient::to_value(&build_task_schema).unwrap_or_default();
@@ -115,7 +115,7 @@ pub struct TaskTemplates(pub Vec<TaskTemplate>);
impl TaskTemplates {
/// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value {
- let schema = schemars::generate::SchemaSettings::draft07()
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
.root_schema_for::<Self>();