[PATCH proxmox v3 02/38] schema: oneOf: allow single string variant

Christoph Heiss
2026-04-03T16:55:00Z

This allows a `OneOfSchema` to additionally have a single string
variant, i.e. allows to deserialize from either a plain string or some
object.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v2 -> v3:
* new patch

proxmox-schema/src/schema.rs | 68 +++++++++++++++++++++++--
proxmox-schema/tests/schema.rs | 91 +++++++++++++++++++++++++++++++++-
2 files changed, 155 insertions(+), 4 deletions(-)

diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs
index 47ee94df..24815bdb 100644
--- a/proxmox-schema/src/schema.rs
+++ b/proxmox-schema/src/schema.rs
@@ -912,6 +912,20 @@ const fn assert_one_of_list_is_sorted(list: &[(&str, &Schema)]) {
}
}

+const fn assert_one_of_zero_or_one_string_schema(list: &[(&str, &Schema)]) {
+ let mut i = 0;
+ let mut already_seen = false;
+ while i != list.len() {
+ if let Schema::String(_) = list[i].1 {
+ if already_seen {
+ panic!("oneOf can have only zero or one string variants");
+ }
+ already_seen = true;
+ }
+ i += 1;
+ }
+}
+
impl OneOfSchema {
/// Create a new `oneOf` schema.
///
@@ -947,6 +961,27 @@ impl OneOfSchema {
/// ).schema();
/// ```
///
+ /// There is also support for the data to be either a string or some object:
+ ///
+ /// ```
+ /// # use proxmox_schema::{OneOfSchema, ObjectSchema, Schema, StringSchema};
+ /// # const SCHEMA_V1: Schema = ObjectSchema::new(
+ /// # "Some Object",
+ /// # &[
+ /// # ("key1", false, &StringSchema::new("A String").schema()),
+ /// # ("key2", false, &StringSchema::new("Another String").schema()),
+ /// # ],
+ /// # ).schema();
+ /// const SCHEMA: Schema = OneOfSchema::new(
+ /// "A plain string or some enum",
+ /// &("type", false, &StringSchema::new("v1 or v2").schema()),
+ /// &[
+ /// ("plain-string", &StringSchema::new("some string").schema()),
+ /// ("v1", &SCHEMA_V1),
+ /// ],
+ /// ).schema();
+ /// ```
+ ///
/// These will panic:
///
/// ```compile_fail,E0080
@@ -1001,12 +1036,28 @@ impl OneOfSchema {
/// ],
/// ).schema();
/// ```
+ ///
+ /// ```compile_fail,E0080
+ /// # use proxmox_schema::{OneOfSchema, ObjectSchema, Schema, StringSchema};
+ /// # const SCHEMA_V1: Schema = &StringSchema::new("A String").schema()
+ /// # const SCHEMA_V2: Schema = &StringSchema::new("Another String").schema()
+ /// const SCHEMA: Schema = OneOfSchema::new(
+ /// "Some enum",
+ /// &("type", false, &StringSchema::new("v1 or v2").schema()),
+ /// &[
+ /// ("v1", &SCHEMA_V1),
+ /// // more than one string schema:
+ /// ("v2", &SCHEMA_V2),
+ /// ],
+ /// ).schema();
+ /// ```
pub const fn new(
description: &'static str,
type_property_entry: &'static SchemaPropertyEntry,
list: &'static [(&'static str, &'static Schema)],
) -> Self {
assert_one_of_list_is_sorted(list);
+ assert_one_of_zero_or_one_string_schema(list);
Self {
description,
type_property_entry,
@@ -1065,6 +1116,12 @@ impl OneOfSchema {
) -> Result<Value, ParameterError> {
ParameterSchema::from(self).parse_parameter_strings(data, test_required)
}
+
+ fn string_variant(&self) -> Option<&Schema> {
+ self.list
+ .iter()
+ .find_map(|(_, item)| matches!(item, Schema::String(_)).then_some(&**item))
+ }
}

mod private {
@@ -1271,11 +1328,12 @@ impl ObjectSchemaType for OneOfSchema {
}

fn additional_properties(&self) -> bool {
- self.list.iter().any(|(_, schema)| {
- schema
+ self.list.iter().any(|(_, schema)| match schema {
+ Schema::String(_) => false,
+ _ => schema
.any_object()
.expect("non-object-schema in `OneOfSchema`")
- .additional_properties()
+ .additional_properties(),
})
}

@@ -1286,6 +1344,10 @@ impl ObjectSchemaType for OneOfSchema {
fn verify_json(&self, data: &Value) -> Result<(), Error> {
let map = match data {
Value::Object(map) => map,
+ Value::String(_) => match self.string_variant() {
+ Some(schema) => return schema.verify_json(data),
+ None => bail!("Expected object - got string value."),
+ },
Value::Array(_) => bail!("Expected object - got array."),
_ => bail!("Expected object - got scalar value."),
};
diff --git a/proxmox-schema/tests/schema.rs b/proxmox-schema/tests/schema.rs
index 24c32bef..22d6538e 100644
--- a/proxmox-schema/tests/schema.rs
+++ b/proxmox-schema/tests/schema.rs
@@ -1,5 +1,5 @@
use anyhow::bail;
-use serde_json::Value;
+use serde_json::{json, Value};
use url::form_urlencoded;

use proxmox_schema::*;
@@ -390,3 +390,92 @@ fn test_verify_complex_array() {
assert!(res.is_err());
}
}
+
+#[test]
+fn test_one_of_schema_string_variant() {
+ const OBJECT1_SCHEMA: Schema = ObjectSchema::new(
+ "Object 1",
+ &[
+ ("a", false, &StringSchema::new("A property").schema()),
+ ("type", false, &StringSchema::new("v1 or v2").schema()),
+ ],
+ )
+ .schema();
+ const OBJECT2_SCHEMA: Schema = ObjectSchema::new(
+ "Object 2",
+ &[
+ (
+ "b",
+ true,
+ &StringSchema::new("A optional property").schema(),
+ ),
+ ("type", false, &StringSchema::new("v1 or v2").schema()),
+ ],
+ )
+ .schema();
+
+ const NO_STRING_VARIANT_SCHEMA: OneOfSchema = OneOfSchema::new(
+ "An oneOf schema",
+ &("type", false, &StringSchema::new("v1 or v2").schema()),
+ &[("v1", &OBJECT1_SCHEMA), ("v2", &OBJECT2_SCHEMA)],
+ );
+
+ const ONE_STRING_VARIANT_SCHEMA: OneOfSchema = OneOfSchema::new(
+ "An oneOf schema with a string variant",
+ &(
+ "type",
+ false,
+ &StringSchema::new("string or v1 or v2").schema(),
+ ),
+ &[
+ (
+ "name does not matter",
+ &StringSchema::new("A string").schema(),
+ ),
+ ("v1", &OBJECT1_SCHEMA),
+ ("v2", &OBJECT2_SCHEMA),
+ ],
+ );
+
+ NO_STRING_VARIANT_SCHEMA
+ .verify_json(&json!({
+ "type": "v1", "a": "foo"
+ }))
+ .expect("should verify");
+
+ ONE_STRING_VARIANT_SCHEMA
+ .verify_json(&json!({
+ "type": "v2", "b": "foo"
+ }))
+ .expect("should verify");
+
+ ONE_STRING_VARIANT_SCHEMA
+ .verify_json(&json!("plain string"))
+ .expect("should verify");
+}
+
+#[test]
+#[should_panic(expected = "oneOf can have only zero or one string variants")]
+fn test_one_of_schema_with_multiple_string_variant() {
+ const OBJECT1_SCHEMA: Schema = ObjectSchema::new(
+ "Object 1",
+ &[
+ ("a", false, &StringSchema::new("A property").schema()),
+ ("type", false, &StringSchema::new("v1 or v2").schema()),
+ ],
+ )
+ .schema();
+ const TYPE_SCHEMA: Schema = StringSchema::new("string or string or v1").schema();
+ const STRING1_SCHEMA: Schema = StringSchema::new("A string").schema();
+ const STRING2_SCHEMA: Schema = StringSchema::new("Another string").schema();
+
+ let _ = OneOfSchema::new(
+ "An invalid oneOf schema with multiple string variant",
+ &("type", false, &TYPE_SCHEMA),
+ &[
+ ("string variant 1", &STRING1_SCHEMA),
+ ("v1", &OBJECT1_SCHEMA),
+ ("whoops", &STRING2_SCHEMA),
+ ],
+ );
+}
--
2.53.0