#serialization #serde-derive #derive-deserialize #serde #json #bincode #macro-derive

macro serde-split

Derive two implementations of serde traits for different purposes

4 releases

0.1.3 Oct 2, 2023
0.1.2 Oct 2, 2023
0.1.1 Sep 26, 2023
0.1.0 Sep 26, 2023

#1109 in Encoding

Download history 2/week @ 2024-02-25 40/week @ 2024-03-10 2/week @ 2024-03-17 41/week @ 2024-03-31

83 downloads per month

MIT/Apache

14KB
187 lines

serde-split

serde-split is a helpful wrapper around serde's derive macros for serialization/deserialization.

Using serde-split's versions of the Deserialize and Serialize derive macros will allow the deriver to derive two separate implementations of the trait for use with (de)serializers that properly support skipping fields (such as serde_json) and those that don't (such as bincode).

Examples

Let's say you are creating a game with bevy and have an animation format. In development, you might want this animation format to be easily modifyable JSON data but for official releases you want to package it as binary data.

For the JSON development asset, you'll load each of the keyframes from a path relative to the JSON file, and for the release asset you'll deserialize a spritesheet as PNG data from a binary file.

In this very real use case that inspired this crate, you could achieve it this way:

use serde_split::{Deserialize, Serialize};
use image::RgbaImage;

mod rgba_image {
    /* PNG serde impl */
}

fn default_image() -> RgbaImage {
    RgbaImage::new(1, 1)
}

#[derive(Deserialize, Serialize)]
pub struct SpriteAnimation {
    #[json(skip, default = "default_image")]
    #[bin(with = "rgba_image")]
    pub sprite_sheet: RgbaImage,

    pub keyframes: Vec<KeyFrame>,
}

bincode also doesn't properly support adjacent enum tagging, which would help simplify JSON data for human maintenance but would break binary formats.

You could imagine, also for the same game, that you have some collision object that can have a static position or a path-based position:

use bevy::prelude::Vec2;
use serde::{Deserialize, Serialize};

// This is the only way to declare this while allowing it to work with bincode
#[derive(Deserialize, Serialize)]
pub enum ObjectPosition {
    Static(Vec2),
    Path {
        start: Vec2,
        points: Vec<Vec2>
    }
}

Unfortunately, the above would make our JSON data look like this:

{
    "object": {
        "position": {
            "Static": [0.0, 0.0]
        }
    },
    "object2": {
        "position": {
            "Path": {
                "start": [0.0, 0.0],
                "points": [
                    [10.0, 10.0],
                    [-10.0, 10.0],
                    [0.0, 0.0]
                ]
            }
        }
    }
}

Using serde-split's macros, you could instead declare your struct like this:

use bevy::prelude::Vec2;
use serde_split::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
#[json(untagged)]
pub enum ObjectPosition {
    Static(Vec2),
    Path {
        start: Vec2,
        points: Vec<Vec2>
    }
}

And now your JSON representation gets simplified:

{
    "object": {
        "position": [0.0, 0.0]
    },
    "object2": {
        "position": {
            "start": [0.0, 0.0],
            "points": [
                [10.0, 10.0],
                [-10.0, 10.0],
                [0.0, 0.0]
            ]
        }
    }
}

While the binary representation maintains well-defined for bincode's (de)serializer!

What does it expand to?

If we use the sprite animation example from above, the following struct:

#[derive(Deserialize)]
pub struct SpriteAnimation {
    #[json(skip, default = "default_image")]
    #[bin(with = "rgba_image")]
    pub sprite_sheet: RgbaImage,

    pub keyframes: Vec<KeyFrame>,
}

will roughly expand to:

const _: () = {
    #[derive(serde::Deserialize)]
    #[serde(remote = "SpriteAnimation")]
    pub struct SpriteAnimationJsonImpl {
        #[serde(skip, default = "default_image")]
        pub sprite_sheet: RgbaImage,
        pub keyframes: Vec<KeyFrame>
    }

    #[derive(serde::Deserialize)]
    #[serde(remote = "SpriteAnimation")]
    pub struct SpriteAnimationBinaryImpl {
        #[serde(with = "rgba_image")]
        pub sprite_sheet: RgbaImage,
        pub keyframes: Vec<KeyFrame>
    }

    impl<'de> serde::Deserialize<'de> for SpriteAnimation {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::Deserializer<'de>
        {
            if deserializer.is_human_readable() {
                SpriteAnimationJsonImpl::deserialize(deserializer)
            } else {
                SpriteAnimationBinaryImpl::deserialize(deserializer)
            }
        }
    }
};

With a similar looking expansion for Serialize.

Dependencies

~3MB
~59K SLoC