Skip to content

Commit 03a071a

Browse files
authored
Add support for Range, RangeFrom, RangeTo, RangeInclusive (#861)
2 parents 03c6739 + eea7999 commit 03a071a

File tree

8 files changed

+346
-9
lines changed

8 files changed

+346
-9
lines changed

serde_with/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
* Add support for `Range`, `RangeFrom`, `RangeTo`, `RangeInclusive` (#851)
13+
`RangeToInclusive` is currently unsupported by serde.
14+
* Add `schemars` implementations for `Bound`, `Range`, `RangeFrom`, `RangeTo`, `RangeInclusive`.
15+
1016
## [3.13.0] - 2025-06-14
1117

1218
### Added

serde_with/src/de/impls.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,78 @@ where
408408
}
409409
}
410410

411+
// endregion
412+
///////////////////////////////////////////////////////////////////////////////
413+
// region: More complex wrappers that are not just a single value
414+
415+
impl<'de, Idx, IdxAs> DeserializeAs<'de, Range<Idx>> for Range<IdxAs>
416+
where
417+
IdxAs: DeserializeAs<'de, Idx>,
418+
{
419+
fn deserialize_as<D>(deserializer: D) -> Result<Range<Idx>, D::Error>
420+
where
421+
D: Deserializer<'de>,
422+
{
423+
let Range::<DeserializeAsWrap<Idx, IdxAs>> { start, end } =
424+
Deserialize::deserialize(deserializer)?;
425+
426+
Ok(Range {
427+
start: start.into_inner(),
428+
end: end.into_inner(),
429+
})
430+
}
431+
}
432+
433+
impl<'de, Idx, IdxAs> DeserializeAs<'de, RangeFrom<Idx>> for RangeFrom<IdxAs>
434+
where
435+
IdxAs: DeserializeAs<'de, Idx>,
436+
{
437+
fn deserialize_as<D>(deserializer: D) -> Result<RangeFrom<Idx>, D::Error>
438+
where
439+
D: Deserializer<'de>,
440+
{
441+
let RangeFrom::<DeserializeAsWrap<Idx, IdxAs>> { start } =
442+
Deserialize::deserialize(deserializer)?;
443+
444+
Ok(RangeFrom {
445+
start: start.into_inner(),
446+
})
447+
}
448+
}
449+
450+
impl<'de, Idx, IdxAs> DeserializeAs<'de, RangeInclusive<Idx>> for RangeInclusive<IdxAs>
451+
where
452+
IdxAs: DeserializeAs<'de, Idx>,
453+
{
454+
fn deserialize_as<D>(deserializer: D) -> Result<RangeInclusive<Idx>, D::Error>
455+
where
456+
D: Deserializer<'de>,
457+
{
458+
let (start, end) =
459+
RangeInclusive::<DeserializeAsWrap<Idx, IdxAs>>::deserialize(deserializer)?
460+
.into_inner();
461+
462+
Ok(RangeInclusive::new(start.into_inner(), end.into_inner()))
463+
}
464+
}
465+
466+
impl<'de, Idx, IdxAs> DeserializeAs<'de, RangeTo<Idx>> for RangeTo<IdxAs>
467+
where
468+
IdxAs: DeserializeAs<'de, Idx>,
469+
{
470+
fn deserialize_as<D>(deserializer: D) -> Result<RangeTo<Idx>, D::Error>
471+
where
472+
D: Deserializer<'de>,
473+
{
474+
let RangeTo::<DeserializeAsWrap<Idx, IdxAs>> { end } =
475+
Deserialize::deserialize(deserializer)?;
476+
477+
Ok(RangeTo {
478+
end: end.into_inner(),
479+
})
480+
}
481+
}
482+
411483
// endregion
412484
///////////////////////////////////////////////////////////////////////////////
413485
// region: Collection Types (e.g., Maps, Sets, Vec)

serde_with/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ pub(crate) mod prelude {
395395
fmt::{self, Display},
396396
hash::{BuildHasher, Hash},
397397
marker::PhantomData,
398-
ops::Bound,
398+
ops::{Bound, Range, RangeFrom, RangeInclusive, RangeTo},
399399
option::Option,
400400
pin::Pin,
401401
result::Result,

serde_with/src/schemars_0_8.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,42 @@ where
276276
forward_schema!(HashSet<WrapSchema<T, TA>, S>);
277277
}
278278

279+
impl<T, TA> JsonSchemaAs<Bound<T>> for Bound<TA>
280+
where
281+
TA: JsonSchemaAs<T>,
282+
{
283+
forward_schema!(Bound<WrapSchema<T, TA>>);
284+
}
285+
286+
impl<T, TA> JsonSchemaAs<Range<T>> for Range<TA>
287+
where
288+
TA: JsonSchemaAs<T>,
289+
{
290+
forward_schema!(Range<WrapSchema<T, TA>>);
291+
}
292+
293+
// Note: Not included in `schemars`
294+
// impl<T, TA> JsonSchemaAs<RangeFrom<T>> for RangeFrom<TA>
295+
// where
296+
// TA: JsonSchemaAs<T>,
297+
// {
298+
// forward_schema!(RangeFrom<WrapSchema<T, TA>>);
299+
// }
300+
301+
// impl<T, TA> JsonSchemaAs<RangeTo<T>> for RangeTo<TA>
302+
// where
303+
// TA: JsonSchemaAs<T>,
304+
// {
305+
// forward_schema!(RangeTo<WrapSchema<T, TA>>);
306+
// }
307+
308+
impl<T, TA> JsonSchemaAs<RangeInclusive<T>> for RangeInclusive<TA>
309+
where
310+
TA: JsonSchemaAs<T>,
311+
{
312+
forward_schema!(RangeInclusive<WrapSchema<T, TA>>);
313+
}
314+
279315
impl<T, TA, const N: usize> JsonSchemaAs<[T; N]> for [TA; N]
280316
where
281317
TA: JsonSchemaAs<T>,

serde_with/src/schemars_0_9.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,42 @@ where
282282
forward_schema!(HashSet<WrapSchema<T, TA>, S>);
283283
}
284284

285+
impl<T, TA> JsonSchemaAs<Bound<T>> for Bound<TA>
286+
where
287+
TA: JsonSchemaAs<T>,
288+
{
289+
forward_schema!(Bound<WrapSchema<T, TA>>);
290+
}
291+
292+
impl<T, TA> JsonSchemaAs<Range<T>> for Range<TA>
293+
where
294+
TA: JsonSchemaAs<T>,
295+
{
296+
forward_schema!(Range<WrapSchema<T, TA>>);
297+
}
298+
299+
// Note: Not included in `schemars`
300+
// impl<T, TA> JsonSchemaAs<RangeFrom<T>> for RangeFrom<TA>
301+
// where
302+
// TA: JsonSchemaAs<T>,
303+
// {
304+
// forward_schema!(RangeFrom<WrapSchema<T, TA>>);
305+
// }
306+
307+
// impl<T, TA> JsonSchemaAs<RangeTo<T>> for RangeTo<TA>
308+
// where
309+
// TA: JsonSchemaAs<T>,
310+
// {
311+
// forward_schema!(RangeTo<WrapSchema<T, TA>>);
312+
// }
313+
314+
impl<T, TA> JsonSchemaAs<RangeInclusive<T>> for RangeInclusive<TA>
315+
where
316+
TA: JsonSchemaAs<T>,
317+
{
318+
forward_schema!(RangeInclusive<WrapSchema<T, TA>>);
319+
}
320+
285321
impl<T, TA, const N: usize> JsonSchemaAs<[T; N]> for [TA; N]
286322
where
287323
TA: JsonSchemaAs<T>,

serde_with/src/ser/impls.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,72 @@ where
342342
}
343343
}
344344

345+
// endregion
346+
///////////////////////////////////////////////////////////////////////////////
347+
// region: More complex wrappers that are not just a single value
348+
349+
impl<Idx, IdxAs> SerializeAs<Range<Idx>> for Range<IdxAs>
350+
where
351+
IdxAs: SerializeAs<Idx>,
352+
{
353+
fn serialize_as<S>(value: &Range<Idx>, serializer: S) -> Result<S::Ok, S::Error>
354+
where
355+
S: Serializer,
356+
{
357+
Range {
358+
start: SerializeAsWrap::<Idx, IdxAs>::new(&value.start),
359+
end: SerializeAsWrap::<Idx, IdxAs>::new(&value.end),
360+
}
361+
.serialize(serializer)
362+
}
363+
}
364+
365+
impl<Idx, IdxAs> SerializeAs<RangeFrom<Idx>> for RangeFrom<IdxAs>
366+
where
367+
IdxAs: SerializeAs<Idx>,
368+
{
369+
fn serialize_as<S>(value: &RangeFrom<Idx>, serializer: S) -> Result<S::Ok, S::Error>
370+
where
371+
S: Serializer,
372+
{
373+
RangeFrom {
374+
start: SerializeAsWrap::<Idx, IdxAs>::new(&value.start),
375+
}
376+
.serialize(serializer)
377+
}
378+
}
379+
380+
impl<Idx, IdxAs> SerializeAs<RangeInclusive<Idx>> for RangeInclusive<IdxAs>
381+
where
382+
IdxAs: SerializeAs<Idx>,
383+
{
384+
fn serialize_as<S>(value: &RangeInclusive<Idx>, serializer: S) -> Result<S::Ok, S::Error>
385+
where
386+
S: Serializer,
387+
{
388+
RangeInclusive::new(
389+
SerializeAsWrap::<Idx, IdxAs>::new(value.start()),
390+
SerializeAsWrap::<Idx, IdxAs>::new(value.end()),
391+
)
392+
.serialize(serializer)
393+
}
394+
}
395+
396+
impl<Idx, IdxAs> SerializeAs<RangeTo<Idx>> for RangeTo<IdxAs>
397+
where
398+
IdxAs: SerializeAs<Idx>,
399+
{
400+
fn serialize_as<S>(value: &RangeTo<Idx>, serializer: S) -> Result<S::Ok, S::Error>
401+
where
402+
S: Serializer,
403+
{
404+
RangeTo {
405+
end: SerializeAsWrap::<Idx, IdxAs>::new(&value.end),
406+
}
407+
.serialize(serializer)
408+
}
409+
}
410+
345411
// endregion
346412
///////////////////////////////////////////////////////////////////////////////
347413
// region: Collection Types (e.g., Maps, Sets, Vec)

serde_with/tests/schemars_0_9/main.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Test Cases
22
33
use crate::utils::{check_matches_schema, check_valid_json_schema};
4+
use core::ops;
45
use expect_test::expect_file;
56
use schemars::JsonSchema;
67
use serde::Serialize;
@@ -1074,3 +1075,56 @@ fn test_pickfirst() {
10741075
check_matches_schema::<IntOrDisplay>(&json!(7));
10751076
check_matches_schema::<IntOrDisplay>(&json!("17"));
10761077
}
1078+
1079+
#[test]
1080+
fn test_bound() {
1081+
#[serde_as]
1082+
#[derive(JsonSchema, Serialize)]
1083+
#[serde(transparent)]
1084+
struct S(#[serde_as(as = "ops::Bound<DisplayFromStr>")] ops::Bound<u32>);
1085+
1086+
check_valid_json_schema(&S(ops::Bound::Unbounded));
1087+
check_valid_json_schema(&S(ops::Bound::Included(42)));
1088+
check_valid_json_schema(&S(ops::Bound::Excluded(42)));
1089+
}
1090+
1091+
#[test]
1092+
fn test_range() {
1093+
#[serde_as]
1094+
#[derive(JsonSchema, Serialize)]
1095+
#[serde(transparent)]
1096+
struct S(#[serde_as(as = "ops::Range<DisplayFromStr>")] ops::Range<u32>);
1097+
1098+
check_valid_json_schema(&S(3..7));
1099+
}
1100+
1101+
// Note: Not included in `schemars`
1102+
// #[test]
1103+
// fn test_rangefrom() {
1104+
// #[serde_as]
1105+
// #[derive(JsonSchema, Serialize)]
1106+
// #[serde(transparent)]
1107+
// struct S(#[serde_as(as = "ops::RangeFrom<DisplayFromStr>")] ops::RangeFrom<u32>);
1108+
1109+
// check_valid_json_schema(&S(3..));
1110+
// }
1111+
1112+
// #[test]
1113+
// fn test_rangeto() {
1114+
// #[serde_as]
1115+
// #[derive(JsonSchema, Serialize)]
1116+
// #[serde(transparent)]
1117+
// struct S(#[serde_as(as = "ops::RangeTo<DisplayFromStr>")] ops::RangeTo<u32>);
1118+
1119+
// check_valid_json_schema(&S(..7));
1120+
// }
1121+
1122+
#[test]
1123+
fn test_rangeinclusive() {
1124+
#[serde_as]
1125+
#[derive(JsonSchema, Serialize)]
1126+
#[serde(transparent)]
1127+
struct S(#[serde_as(as = "ops::RangeInclusive<DisplayFromStr>")] ops::RangeInclusive<u32>);
1128+
1129+
check_valid_json_schema(&S(3..=7));
1130+
}

0 commit comments

Comments
 (0)