Skip to main content

wincode/schema/
adapter.rs

1//! Schema adapters that serialize a type by converting it to and from the value
2//! of an intermediate "wire" schema.
3
4#[cfg(feature = "alloc")]
5use crate::len::SeqLen;
6use {
7    crate::{
8        TypeMeta,
9        config::ConfigCore,
10        error::{ReadError, ReadResult, WriteResult},
11        io::{ReadError as IoReadError, Reader, Writer},
12        schema::{SchemaRead, SchemaWrite},
13    },
14    core::{marker::PhantomData, mem::MaybeUninit},
15};
16
17/// Serialize a `Target` by going through an intermediate wire representation
18/// using the standard library [`From`]/[`Into`] conversions.
19///
20/// `Wire` is a *schema* (any `SchemaWrite`/`SchemaRead` implementor), and the
21/// intermediate value is the type that schema serializes — its
22/// [`SchemaWrite::Src`] on write and [`SchemaRead::Dst`] on read. On write the
23/// `Target` is converted into that value (via `From<&Target>`) and serialized
24/// with `Wire`; on read the value is deserialized with `Wire` and converted back
25/// into the `Target` (via `Target: From<…>`).
26///
27/// Because `Wire` is a schema rather than the value itself, the wire side may be:
28/// - a self-describing type, where the value is the type itself — e.g. `i64`, or
29///   any type deriving [`SchemaWrite`](wincode_derive::SchemaWrite) /
30///   [`SchemaRead`](wincode_derive::SchemaRead);
31/// - a schema adapter over some other value — e.g.
32///   [`containers::Vec<u8, UseIntLen<u16>>`](crate::containers::Vec), whose value
33///   is `Vec<u8>`.
34///
35/// The `Target` is the type of the annotated field. Use the `_` inference token
36/// to have it filled in automatically from the field type:
37///
38/// ```
39/// # #[cfg(feature = "derive")] {
40/// # use wincode::adapter::FromInto;
41/// # use wincode_derive::{SchemaWrite, SchemaRead};
42/// // Our in-memory type with a layout we don't want to serialize directly.
43/// #[derive(Debug, PartialEq, Clone, Copy)]
44/// struct Celsius(f64);
45///
46/// // The on-the-wire representation: hundredths of a degree as an integer.
47/// impl From<&Celsius> for i64 {
48///     fn from(c: &Celsius) -> i64 {
49///         (c.0 * 100.0) as i64
50///     }
51/// }
52/// impl From<i64> for Celsius {
53///     fn from(raw: i64) -> Celsius {
54///         Celsius(raw as f64 / 100.0)
55///     }
56/// }
57///
58/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
59/// struct Reading {
60///     #[wincode(with = "FromInto<i64, _>")]
61///     temp: Celsius,
62/// }
63///
64/// let reading = Reading { temp: Celsius(21.5) };
65/// let bytes = wincode::serialize(&reading).unwrap();
66/// assert_eq!(reading, wincode::deserialize(&bytes).unwrap());
67/// # }
68/// ```
69pub struct FromInto<Wire, Target>(PhantomData<Wire>, PhantomData<Target>);
70
71unsafe impl<C, Wire, Target> SchemaWrite<C> for FromInto<Wire, Target>
72where
73    C: ConfigCore,
74    Wire: SchemaWrite<C>,
75    Wire::Src: Sized + for<'a> From<&'a Target>,
76{
77    type Src = Target;
78
79    // The serialized form is exactly the wire value's, so the size is forwarded.
80    // The conversion means the `Target`'s in-memory representation does not match
81    // the serialized form, so `zero_copy` is cleared.
82    const TYPE_META: TypeMeta = <Wire as SchemaWrite<C>>::TYPE_META.keep_zero_copy(false);
83
84    #[inline]
85    fn size_of(src: &Self::Src) -> WriteResult<usize> {
86        if let TypeMeta::Static { size, .. } = <Self as SchemaWrite<C>>::TYPE_META {
87            return Ok(size);
88        }
89        let wire: Wire::Src = src.into();
90        <Wire as SchemaWrite<C>>::size_of(&wire)
91    }
92
93    #[inline]
94    fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
95        let wire: Wire::Src = src.into();
96        <Wire as SchemaWrite<C>>::write(writer, &wire)
97    }
98}
99
100unsafe impl<'de, C, Wire, Target> SchemaRead<'de, C> for FromInto<Wire, Target>
101where
102    C: ConfigCore,
103    Wire: SchemaRead<'de, C>,
104    Target: From<Wire::Dst>,
105{
106    type Dst = Target;
107
108    const TYPE_META: TypeMeta = <Wire as SchemaRead<'de, C>>::TYPE_META.keep_zero_copy(false);
109
110    #[inline]
111    fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
112        let wire = <Wire as SchemaRead<'de, C>>::get(reader)?;
113        dst.write(Target::from(wire));
114        Ok(())
115    }
116}
117
118/// Deserializes using the wire schema `T` normally, but yields
119/// `T::Dst::default()` when the reader is exhausted (hits EOF) instead of
120/// erroring.
121///
122/// The purpose is backward compatibility when new fields are appended to the
123/// tail of a persisted struct: older, shorter encodings that predate those
124/// fields still decode, with the missing trailing fields filled from their
125/// [`Default`] value. Reading a full encoding is unaffected.
126///
127/// Writing is unchanged — the value is serialized exactly as `T` would,
128/// producing bytes that decode identically with or without this adapter. Only
129/// the read path differs, so this is purely a decode-time compatibility shim.
130///
131/// Apply it via the `with` attribute on the (necessarily trailing) fields it
132/// covers, naming the field's own schema as `T`:
133///
134/// ```
135/// # #[cfg(feature = "derive")] {
136/// # use wincode::adapter::DefaultOnEmptyRead;
137/// # use wincode_derive::{SchemaWrite, SchemaRead};
138/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
139/// struct Record {
140///     id: u32,
141///     // Appended in a later version; older encodings omit it entirely.
142///     #[wincode(with = "DefaultOnEmptyRead<u64>")]
143///     added_later: u64,
144/// }
145///
146/// // A full encoding round-trips as usual.
147/// let record = Record { id: 7, added_later: 42 };
148/// let bytes = wincode::serialize(&record).unwrap();
149/// assert_eq!(record, wincode::deserialize(&bytes).unwrap());
150///
151/// // An older encoding that predates `added_later` decodes to its default.
152/// let legacy = wincode::serialize(&7u32).unwrap();
153/// let decoded: Record = wincode::deserialize(&legacy).unwrap();
154/// assert_eq!(decoded, Record { id: 7, added_later: 0 });
155/// # }
156/// ```
157///
158/// # Warning
159///
160/// The fallback is driven purely by running out of bytes, so it is only sound
161/// where "no more bytes" unambiguously means "this optional tail is absent":
162///
163/// - **Do not use it on sequence elements.** When more items follow, a missing
164///   field does not produce EOF — the read simply continues into the bytes that
165///   encode the *next* item. Instead of defaulting the absent field, the decoder
166///   consumes the following item's data, desynchronizing the rest of the
167///   sequence. The fallback only helps when the missing bytes are genuinely the
168///   end of input.
169/// - **Do not use it on a middle field followed by an always-present field.**
170///   The fallback catches *any* size-limit error from `T`, including a
171///   partially-present value, so a genuinely truncated field is masked instead
172///   of reported and the fields after it are misaligned. It is only safe on a
173///   trailing run of fields where every field from the first
174///   `DefaultOnEmptyRead` onward is itself optional-on-EOF.
175///
176/// Prefer applying it to trailing fields of a **top-level struct** decoded with
177/// [`deserialize_exact`](crate::deserialize_exact), where reaching EOF exactly
178/// at a field boundary is well defined and the end-of-input check is
179/// straightforward and cheap.
180pub struct DefaultOnEmptyRead<T>(PhantomData<T>);
181
182unsafe impl<'de, C, T> SchemaRead<'de, C> for DefaultOnEmptyRead<T>
183where
184    C: ConfigCore,
185    T: SchemaRead<'de, C>,
186    T::Dst: Default,
187{
188    type Dst = T::Dst;
189
190    // TYPE_META is intentionally left at the default `Dynamic`: decoding may read
191    // either 0 bytes (the EOF fallback) or `T`'s full encoding, so the decoded
192    // size is not fixed and a reader must not prefetch a static size.
193
194    #[inline]
195    fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
196        match <T as SchemaRead<'de, C>>::read(reader, dst) {
197            Ok(()) => Ok(()),
198            Err(ReadError::Io(IoReadError::ReadSizeLimit(_))) => {
199                dst.write(Self::Dst::default());
200                Ok(())
201            }
202            Err(e) => Err(e),
203        }
204    }
205}
206
207unsafe impl<C, T> SchemaWrite<C> for DefaultOnEmptyRead<T>
208where
209    C: ConfigCore,
210    T: SchemaWrite<C>,
211{
212    type Src = T::Src;
213
214    const TYPE_META: TypeMeta = <T as SchemaWrite<C>>::TYPE_META;
215
216    #[inline]
217    fn size_of(src: &Self::Src) -> WriteResult<usize> {
218        <T as SchemaWrite<C>>::size_of(src)
219    }
220
221    #[inline]
222    fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
223        <T as SchemaWrite<C>>::write(writer, src)
224    }
225}
226
227/// Reads a value with the `Inner` schema purely to advance the reader, then throws
228/// it away and yields [`Default::default()`] instead.
229///
230/// `Inner` is a *schema*, its bytes are consumed exactly as `Inner` would consume them
231/// — including any validation `Inner` performs — so the surrounding stream stays in sync,
232/// but the decoded value is dropped and the field is filled with the [`Default`] of `Inner`'s
233/// [`SchemaRead::Dst`].
234///
235/// It is meant for fields you don't want to persist, such as a deprecated field
236/// kept only for wire compatibility. On read the encoded value is stepped over and
237/// reset to its [`Default`]; on write the value held in the instance is likewise
238/// ignored and `Inner`'s [`Default`] is serialized in its place. The two directions
239/// agree, so `Discard` works on types deriving both
240/// [`SchemaWrite`](wincode_derive::SchemaWrite) and [`SchemaRead`] — with the
241/// caveat that the original value is *not* preserved across a round-trip: a
242/// discarded field always reads back, and re-serializes, as the default.
243///
244/// For a length-prefixed sequence, prefer [`DiscardSeq`], which steps over every
245/// element without allocating a backing buffer.
246///
247/// ```
248/// # #[cfg(feature = "derive")] {
249/// # use wincode::adapter::Discard;
250/// # use wincode_derive::{SchemaWrite, SchemaRead};
251/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
252/// struct Full {
253///     ignored: u32,
254///     kept: u16,
255/// }
256///
257/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
258/// struct Partial {
259///     #[wincode(with = "Discard<u32>")]
260///     ignored: u32,
261///     kept: u16,
262/// }
263///
264/// // On read, `ignored` is stepped over and reset to its default.
265/// let bytes = wincode::serialize(&Full { ignored: 7, kept: 9 }).unwrap();
266/// assert_eq!(
267///     wincode::deserialize::<Partial>(&bytes).unwrap(),
268///     Partial { ignored: 0, kept: 9 },
269/// );
270///
271/// // On write, the held value of `ignored` is discarded and its default is
272/// // serialized, so the output matches a `Full` whose `ignored` is `0`.
273/// let out = wincode::serialize(&Partial { ignored: 7, kept: 9 }).unwrap();
274/// assert_eq!(out, wincode::serialize(&Full { ignored: 0, kept: 9 }).unwrap());
275/// # }
276/// ```
277pub struct Discard<Inner>(PhantomData<Inner>);
278
279unsafe impl<'de, C, Inner> SchemaRead<'de, C> for Discard<Inner>
280where
281    C: ConfigCore,
282    Inner: SchemaRead<'de, C>,
283    Inner::Dst: Default,
284{
285    type Dst = Inner::Dst;
286
287    // Not zero-copy: a fresh `Default` is yielded, not a view of the wire bytes.
288    const TYPE_META: TypeMeta = Inner::TYPE_META.keep_zero_copy(false);
289
290    #[inline]
291    fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
292        // No byte-skip fast path (unlike `DiscardSeq`): a single value has no
293        // per-element bounds checks to hoist, and dropping the decoded value lets
294        // the optimizer elide the copy out of the reader.
295        Inner::get(reader)?;
296        dst.write(Default::default());
297        Ok(())
298    }
299}
300
301unsafe impl<C, Inner> SchemaWrite<C> for Discard<Inner>
302where
303    C: ConfigCore,
304    Inner: SchemaWrite<C>,
305    Inner::Src: Default,
306{
307    type Src = Inner::Src;
308
309    // Not zero-copy: the `Default` is written, not the instance's own bytes.
310    const TYPE_META: TypeMeta = Inner::TYPE_META.keep_zero_copy(false);
311
312    #[inline]
313    fn size_of(_src: &Self::Src) -> WriteResult<usize> {
314        if let TypeMeta::Static { size, .. } = Inner::TYPE_META {
315            return Ok(size);
316        }
317        Inner::size_of(&Default::default())
318    }
319
320    #[inline]
321    fn write(writer: impl Writer, _src: &Self::Src) -> WriteResult<()> {
322        // Ignore the provided value and match what `read` reconstructs.
323        Inner::write(writer, &Default::default())
324    }
325}
326
327/// Discards a length-prefixed sequence: reads the length (encoded per `Len`) and
328/// steps over every element with the element schema `T`, advancing the reader
329/// exactly as decoding the sequence would, yet never allocates a backing buffer and
330/// always yields an empty [`Vec`].
331///
332/// Every wincode sequence — `Vec`, `String`, slices, sets, and maps — shares the
333/// same *length prefix + elements* wire layout, so this discards **any** of them;
334/// `T` only names one element's wire form:
335///
336/// - a `Vec<u32>`: `DiscardSeq<u32, Len>`;
337/// - a `String`: `DiscardSeq<u8, Len>` (identical bytes to a `Vec<u8>`);
338/// - a map such as `HashMap<K, V>` / `BTreeMap<K, V>`: `DiscardSeq<(K, V), Len>`,
339///   since each entry is encoded as a `(K, V)` pair;
340/// - nested sequences, with no allocation at any level:
341///   `DiscardSeq<DiscardSeq<u8, L2>, L1>` for a `Vec<Vec<u8>>`.
342///
343/// The produced value is always an empty `Vec<T::Dst>`, and that type is
344/// incidental. Deprecating a field is then just repointing it at `DiscardSeq` and
345/// changing its type to the matching `Vec<…>`: the field is dead, so its concrete
346/// collection type no longer matters — only that the bytes are consumed and the
347/// stream stays aligned.
348///
349/// The length prefix is still validated against the configured preallocation
350/// limit, so a hostile length errors out identically to a real read.
351///
352/// Like [`Discard`], the sequence held in the instance is ignored on write and an
353/// empty sequence is emitted, matching what `read` reconstructs. Use the
354/// configuration's default length encoding, e.g.
355/// [`BincodeLen`](crate::len::BincodeLen), to match how the field was originally
356/// encoded.
357///
358/// ```
359/// # #[cfg(all(feature = "alloc", feature = "derive"))] {
360/// # use wincode::{adapter::DiscardSeq, len::BincodeLen};
361/// # use wincode_derive::{SchemaWrite, SchemaRead};
362/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
363/// struct Full {
364///     data: Vec<u32>,
365///     tag: u8,
366/// }
367///
368/// #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
369/// struct Partial {
370///     #[wincode(with = "DiscardSeq<u32, BincodeLen>")]
371///     data: Vec<u32>,
372///     tag: u8,
373/// }
374///
375/// // On read, `data` is stepped over and reset to an empty `Vec`.
376/// let bytes = wincode::serialize(&Full { data: vec![1, 2, 3], tag: 42 }).unwrap();
377/// assert_eq!(
378///     wincode::deserialize::<Partial>(&bytes).unwrap(),
379///     Partial { data: Vec::new(), tag: 42 },
380/// );
381///
382/// // On write, `data` is discarded and an empty sequence is serialized, so the
383/// // output matches a `Full` with empty `data`.
384/// let out = wincode::serialize(&Partial { data: vec![1, 2, 3], tag: 42 }).unwrap();
385/// assert_eq!(out, wincode::serialize(&Full { data: vec![], tag: 42 }).unwrap());
386/// # }
387/// ```
388#[cfg(feature = "alloc")]
389pub struct DiscardSeq<T, Len>(PhantomData<T>, PhantomData<Len>);
390
391#[cfg(feature = "alloc")]
392unsafe impl<'de, C, T, Len> SchemaRead<'de, C> for DiscardSeq<T, Len>
393where
394    C: ConfigCore,
395    Len: SeqLen<C>,
396    T: SchemaRead<'de, C>,
397{
398    type Dst = alloc::vec::Vec<T::Dst>;
399
400    #[inline]
401    fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
402        let len = Len::read_prealloc_check::<T::Dst>(reader.by_ref())?;
403        if let TypeMeta::Static { size, .. } = T::TYPE_META {
404            // Reserve the whole `len * size`-byte body up front so the per-element
405            // decodes below elide their own bounds checks. `get` still validates
406            // each element, so non-zero-copy types are handled correctly too.
407            //
408            // SAFETY: exactly `len` decodes of `size` bytes run through the trusted window
409            let mut trusted = unsafe { reader.as_trusted_for_seq(len, size)? };
410            for _ in 0..len {
411                T::get(trusted.by_ref())?;
412            }
413        } else {
414            for _ in 0..len {
415                T::get(reader.by_ref())?;
416            }
417        }
418        dst.write(alloc::vec::Vec::new());
419        Ok(())
420    }
421}
422
423#[cfg(feature = "alloc")]
424unsafe impl<C, T, Len> SchemaWrite<C> for DiscardSeq<T, Len>
425where
426    C: ConfigCore,
427    Len: SeqLen<C>,
428    T: SchemaWrite<C>,
429    T::Src: Sized,
430{
431    type Src = alloc::vec::Vec<T::Src>;
432
433    #[inline]
434    fn size_of(_src: &Self::Src) -> WriteResult<usize> {
435        // An empty sequence: just the length prefix for 0.
436        Len::write_bytes_needed(0)
437    }
438
439    #[inline]
440    fn write(writer: impl Writer, _src: &Self::Src) -> WriteResult<()> {
441        // Ignore the provided sequence; emit an empty one, matching `read`.
442        Len::write(writer, 0)
443    }
444}
445
446#[cfg(all(test, feature = "derive", feature = "alloc"))]
447mod tests {
448    use {
449        crate::{
450            SchemaRead, SchemaWrite,
451            adapter::{DefaultOnEmptyRead, Discard, DiscardSeq, FromInto},
452            deserialize,
453            len::BincodeLen,
454            serialize,
455        },
456        alloc::{collections::BTreeMap, string::String, vec::Vec},
457    };
458
459    /// A self-describing wire schema: the wire value is a plain `u32`.
460    #[test]
461    fn scalar_wire() {
462        #[derive(Debug, PartialEq, Clone, Copy)]
463        struct Id(u32);
464
465        impl From<&Id> for u32 {
466            fn from(id: &Id) -> u32 {
467                id.0
468            }
469        }
470        impl From<u32> for Id {
471            fn from(raw: u32) -> Id {
472                Id(raw)
473            }
474        }
475
476        #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
477        #[wincode(internal)]
478        struct Msg {
479            #[wincode(with = "FromInto<u32, _>")]
480            id: Id,
481        }
482
483        let msg = Msg {
484            id: Id(0xdead_beef),
485        };
486        let bytes = serialize(&msg).unwrap();
487        // Serialized form is identical to a bare `u32`.
488        assert_eq!(bytes, serialize(&0xdead_beef_u32).unwrap());
489        assert_eq!(msg, deserialize(&bytes).unwrap());
490    }
491
492    /// `Wire` is a schema *adapter* whose value type differs from itself:
493    /// `containers::Vec<u8, _>` serializes a `Vec<u8>`.
494    #[test]
495    fn adapter_wire() {
496        use crate::{containers, len::UseIntLen};
497
498        #[derive(Debug, PartialEq, Clone)]
499        struct Name(String);
500
501        impl From<&Name> for Vec<u8> {
502            fn from(name: &Name) -> Vec<u8> {
503                name.0.as_bytes().to_vec()
504            }
505        }
506        impl From<Vec<u8>> for Name {
507            fn from(bytes: Vec<u8>) -> Name {
508                Name(String::from_utf8(bytes).unwrap())
509            }
510        }
511
512        #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
513        #[wincode(internal)]
514        struct Msg {
515            #[wincode(with = "FromInto<containers::Vec<u8, UseIntLen<u16>>, _>")]
516            name: Name,
517        }
518
519        let msg = Msg {
520            name: Name("wincode".into()),
521        };
522        let bytes = serialize(&msg).unwrap();
523        // u16 length prefix + the raw bytes.
524        assert_eq!(bytes.len(), 2 + "wincode".len());
525        assert_eq!(msg, deserialize(&bytes).unwrap());
526    }
527
528    #[test]
529    fn default_on_empty_read() {
530        #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
531        #[wincode(internal)]
532        struct Record {
533            id: u32,
534            #[wincode(with = "DefaultOnEmptyRead<u64>")]
535            added_later: u64,
536        }
537
538        // Full encoding round-trips unchanged, and writing is identical to a
539        // plain `u32` + `u64`.
540        let record = Record {
541            id: 7,
542            added_later: 42,
543        };
544        let bytes = serialize(&record).unwrap();
545        assert_eq!(bytes.len(), 4 + 8);
546        assert_eq!(record, deserialize(&bytes).unwrap());
547
548        // A legacy encoding that stops after `id` decodes `added_later` to its
549        // default rather than erroring on EOF.
550        let legacy = serialize(&7u32).unwrap();
551        assert_eq!(
552            deserialize::<Record>(&legacy).unwrap(),
553            Record {
554                id: 7,
555                added_later: 0,
556            }
557        );
558
559        // The fallback triggers on any `ReadSizeLimit`, so a *partially* present
560        // inner value (not just a clean field boundary) also decodes to the
561        // default. This is partly deliberate and partly a consequence of the
562        // reader interface: readers do not expose how many bytes remain, and
563        // probing that is neither efficient nor guaranteed to work across all
564        // reader implementations, so we cannot distinguish a clean boundary from
565        // a truncated field. Here 4 bytes cover `id` and the stray tail is too
566        // short for the `u64`.
567        let truncated = [0u8; 6];
568        assert_eq!(
569            deserialize::<Record>(&truncated).unwrap(),
570            Record {
571                id: 0,
572                added_later: 0,
573            }
574        );
575    }
576
577    #[test]
578    fn discard_seq_nested() {
579        #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
580        #[wincode(internal)]
581        struct Full {
582            pairs: Vec<(u32, u16)>,
583            singles: Vec<u32>,
584            nested: Vec<Vec<u8>>,
585            map: BTreeMap<u32, u64>,
586            tag: u8,
587        }
588
589        #[derive(SchemaRead, Debug, PartialEq)]
590        #[wincode(internal)]
591        struct Partial {
592            // Composite static element via the trusted-window path.
593            #[wincode(with = "DiscardSeq<(u32, u16), BincodeLen>")]
594            pairs: Vec<(u32, u16)>,
595            // `Discard` composed as the element schema.
596            #[wincode(with = "DiscardSeq<Discard<u32>, BincodeLen>")]
597            singles: Vec<u32>,
598            // Nested `DiscardSeq`: discards `Vec<Vec<u8>>` with no allocation at
599            // any level while still yielding `Vec<Vec<u8>>`.
600            #[wincode(with = "DiscardSeq<DiscardSeq<u8, BincodeLen>, BincodeLen>")]
601            nested: Vec<Vec<u8>>,
602            // A map is a sequence of `(K, V)` entries; the field type is now an
603            // incidental `Vec`.
604            #[wincode(with = "DiscardSeq<(u32, u64), BincodeLen>")]
605            map: Vec<(u32, u64)>,
606            tag: u8,
607        }
608
609        let bytes = serialize(&Full {
610            pairs: vec![(1, 2), (3, 4)],
611            singles: vec![10, 20, 30],
612            nested: vec![vec![1, 2], vec![], vec![9]],
613            map: BTreeMap::from([(1, 10), (2, 20), (3, 30)]),
614            tag: 7,
615        })
616        .unwrap();
617        assert_eq!(
618            deserialize::<Partial>(&bytes).unwrap(),
619            Partial {
620                pairs: Vec::new(),
621                singles: Vec::new(),
622                nested: Vec::new(),
623                map: Vec::new(),
624                tag: 7,
625            },
626        );
627    }
628
629    #[test]
630    fn discard_seq_validates_non_zero_copy_elements() {
631        #[derive(SchemaRead, Debug, PartialEq)]
632        #[wincode(internal)]
633        struct Partial {
634            #[wincode(with = "DiscardSeq<bool, BincodeLen>")]
635            flags: Vec<bool>,
636        }
637
638        let ok = serialize(&vec![true, false]).unwrap();
639        assert_eq!(
640            deserialize::<Partial>(&ok).unwrap(),
641            Partial { flags: Vec::new() },
642        );
643
644        // A non-canonical bool byte must still be rejected, not skipped.
645        let mut bad = ok.clone();
646        *bad.last_mut().unwrap() = 2;
647        assert!(deserialize::<Partial>(&bad).is_err());
648    }
649
650    #[test]
651    fn discard_string_as_bytes() {
652        #[derive(SchemaWrite, SchemaRead, Debug, PartialEq)]
653        #[wincode(internal)]
654        struct Full {
655            text: String,
656            tag: u8,
657        }
658
659        // A `String` shares its wire format with `Vec<u8>`, so it is discarded as
660        // raw bytes via `DiscardSeq<u8>`: no allocation, and the following field
661        // stays aligned.
662        #[derive(SchemaRead, Debug, PartialEq)]
663        #[wincode(internal)]
664        struct Partial {
665            #[wincode(with = "DiscardSeq<u8, BincodeLen>")]
666            text: Vec<u8>,
667            tag: u8,
668        }
669
670        let bytes = serialize(&Full {
671            text: "hello".into(),
672            tag: 7,
673        })
674        .unwrap();
675        assert_eq!(
676            deserialize::<Partial>(&bytes).unwrap(),
677            Partial {
678                text: Vec::new(),
679                tag: 7,
680            },
681        );
682    }
683}