From 50e70a064d9d49282fba6c979aeaedf9f2d95096 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 9 Sep 2026 10:34:27 -0700 Subject: [PATCH] Add the trait `OptionalNullable` --- CHANGELOG.md | 4 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 37 +++++++++++++++++++++++++++++++++-- 5 files changed, 93 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c541b2b..c1e99e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next +## [0.0.1-alpha.2] - 2026-09-09 + +* Added the trait `OptionalNullable` to model absent, null, or a value + ## [0.0.1-alpha.1] - 2026-08-18 * Initial release diff --git a/Cargo.lock b/Cargo.lock index dcd72bd..a3dbd95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "json-serde" -version = "0.0.1-alpha.1" +version = "0.0.1-alpha.2" dependencies = [ "schemars 0.8.22", "schemars 1.2.2", diff --git a/Cargo.toml b/Cargo.toml index 09d3c1a..8191a25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json-serde" -version = "0.0.1-alpha.1" +version = "0.0.1-alpha.2" edition = "2024" rust-version = "1.97" license = "Apache-2.0" diff --git a/README.md b/README.md index 2aaac03..a64370b 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,64 @@ with `#[serde(default)]` it distinguishes absent from `null`. Applied to an ```rust #[derive(serde::Deserialize, serde::Serialize)] -struct Foo { +struct Data { /// may be absent, but may not be null #[serde( default, deserialize_with = "::json_serde::deserialize_some", skip_serializing_if = "Option::is_none", )] - field: Option, + required_field: Option, + + /// distinct states for absent, null, and a value + #[serde( + default, + deserialize_with = "::json_serde::deserialize_some", + skip_serializing_if = "Option::is_none" + )] + tri_state_field: Option>, +} +``` + +In addition, the `OptionalNullable` trait can be implemented for types that +model this tri-state of absent, null, or a value. It has a default +implementation for `Option>`. + +```rust +#[derive(Default, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum OptionField { + #[default] + #[serde(skip)] + Absent, + Null, + Present(T), +} + +impl json_serde::OptionalNullable for OptionField { + type Target = T; + + fn is_absent(&self) -> bool { + matches!(self, OptionField::Absent) + } + + fn null() -> Self { + Self::Null + } + + fn value(value: Self::Target) -> Self { + Self::Present(value) + } +} + +#[derive(serde::Deserialize, serde::Serialize)] +struct Data { + /// default constructs an absent value; deserialize accepts a null or value + #[serde( + default, + skip_serializing_if = "::json_serde::OptionalNullable::is_absent" + )] + optional_option: OptionField, } ``` diff --git a/src/lib.rs b/src/lib.rs index 77b8d07..8ca7d30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ use serde_core::{ /// /// ``` /// # #[derive(serde::Deserialize, serde::Serialize)] -/// # struct Foo { +/// # struct Data { /// #[serde( /// default, /// deserialize_with = "::json_serde::deserialize_some", @@ -37,7 +37,7 @@ use serde_core::{ /// was absent, `null`, or had a value: /// ``` /// # #[derive(serde::Deserialize, serde::Serialize)] -/// # struct Foo { +/// # struct Data { /// #[serde( /// default, /// deserialize_with = "::json_serde::deserialize_some", @@ -434,6 +434,39 @@ impl schemars1::JsonSchema for Absent { } } +/// Models a fields that may be absent, null, or a value. +/// +/// `Default` is required; it constructs the absent state. +pub trait OptionalNullable: Default { + /// The type for the value when present and non-null. + type Target; + + /// Returns true if the instance represents an absent value. + fn is_absent(&self) -> bool; + + /// Construct a null. + fn null() -> Self; + + /// Construct a value. + fn value(value: Self::Target) -> Self; +} + +impl OptionalNullable for Option> { + type Target = T; + + fn is_absent(&self) -> bool { + self.is_none() + } + + fn null() -> Self { + Some(None) + } + + fn value(value: Self::Target) -> Self { + Some(Some(value)) + } +} + #[cfg(test)] mod tests { use serde::{Deserialize, Serialize, ser::SerializeSeq};