risingwave_common/config/
none_as_empty_string.rs

1// Copyright 2026 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Serialize and deserialize `Option<T>` as empty string if it is `None`. This is useful to:
16//!
17//! - Demonstrate config entries whose default values are `None` in `example.toml`
18//! - Allow users to override a config entry that's already set to `Some` back to `None` in
19//!   per-job configuration via `ALTER .. SET CONFIG`
20//!
21//! Note that using this utility on a `String` should be carefully considered, as it will
22//! confuse explicit empty string and `None`.
23
24use serde::de::Error as _;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27pub fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
28where
29    S: Serializer,
30    T: Serialize,
31{
32    match value {
33        Some(t) => t.serialize(serializer),
34        None => serializer.serialize_str(""),
35    }
36}
37
38pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
39where
40    D: Deserializer<'de>,
41    T: Deserialize<'de>,
42{
43    use serde_content::{Deserializer, Value};
44
45    // Collect as `serde_content::Value` first to check if it is empty string.
46    let v = Value::deserialize(deserializer)?;
47
48    if let Value::String(s) = &v
49        && s.is_empty()
50    {
51        return Ok(None);
52    }
53
54    // If it's not empty string, deserialize it again as `T`.
55    let t = Deserializer::new(v)
56        .human_readable()
57        .coerce_numbers()
58        .deserialize()
59        .map_err(D::Error::custom)?;
60
61    Ok(Some(t))
62}
63
64#[cfg(test)]
65mod tests {
66    use expect_test::expect;
67    use serde_default::DefaultFromSerde;
68
69    use super::*;
70
71    fn default_b() -> Option<usize> {
72        Some(42)
73    }
74
75    #[derive(Serialize, Deserialize, PartialEq, Debug, DefaultFromSerde)]
76    struct Config {
77        #[serde(with = "super")]
78        #[serde(default)]
79        a: Option<usize>,
80
81        #[serde(with = "super")]
82        #[serde(default = "default_b")]
83        b: Option<usize>,
84    }
85
86    #[test]
87    fn test_basic() {
88        let config = Config::default();
89        let toml = toml::to_string(&config).unwrap();
90        expect![[r#"
91            a = ""
92            b = 42
93        "#]]
94        .assert_eq(&toml);
95
96        let config2: Config = toml::from_str(&toml).unwrap();
97        assert_eq!(config2, config);
98    }
99}