#5301Requested

Full Algebraic Type Support

Requested by @tyrantlink
Energy Committed35,055TeVfrom 3 boosters

This is somewhat related to #4031, but I would like full type support for enums, mainly named field variants

enum Birthday {
    Custom(String),
    // ? currently unsupported
    Date {
        day: u8,
        month: u8,
        year: Option<u16>
    }
}

as well as tuple variants

enum Birthday {
    Date(u8, u8, Option<u16>),
    ...
}

both currently error with must be a unit variant or a newtype variant

You can get around this by creating a struct and wrapping it as a newtype variant, but for types that will only ever be used as a single enum variant, that's not ideal.

In my testing manually implementing the derive macro, sats seems to support named field variants just fine, and with only that change you can deploy to maincloud without issue, reads and writes all work, only the module codegen step currently fails.

I haven't tested tuple variants, as I don't personally need them, they're just listed here to complete the type support.

  1. website-features added the feature-request label Jun 13, 2026
  2. you can already do that with

    #[derive(SpacetimeType)]
    pub struct  Date {
            day: u8,
            month: u8,
            year: Option<u16>
        }
    
    enum Birthday {
        Date(Date),
    }
    

    the requirement is just that the custom struct derives SpacetimeType

  3. Yes that was mentioned, but it does require that new struct, Date, be created, imported, and used downstream, any time you need to create the enum, or match on it's contents. In some cases, that would be the singular purpose of the date struct, and it wouldn't be used elsewhere.

    Since these types are supported by sats via nesting AlgebraicType::product within AlgebraicType::sum, the macro would only need to silently create that date struct, solely for the purpose of (de)serialization

    It is a relatively small thing, but since it's supported by sats, it should probably be implemented eventually just to be feature complete. I'm just unsure of what it would be like to create/codegen these types in other languages, since I mostly only know rust and a little typescript

    With the example below, provided you don't try to also generate code, this will deploy to a spacetime server without issue. A codegen attempt will error.

    Example, loosely based on the macro expansion
    use core::any::TypeId;
    use std::marker::PhantomData;
    
    use spacetimedb::{
        Serialize,
        sats::{
            de::{
                FieldNameVisitor,
                NamedProductAccess,
                ProductVisitor,
                SeqProductAccess
            },
            ser::Serializer
        },
        spacetimedb_lib::{
            SpacetimeType,
            de::{
                Deserialize,
                Deserializer,
                Error as DeError,
                SumAccess,
                SumVisitor,
                VariantAccess,
                VariantVisitor
            },
            sats::{AlgebraicType, typespace::TypespaceBuilder},
            ser::SerializeNamedProduct
        }
    };
    
    
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub enum Birthday {
        Custom(String),
        Date {
            day:   u8,
            month: u8,
            year:  Option<u16>
        }
    }
    
    const _: () = {
        struct __DateDePayload {
            day:   u8,
            month: u8,
            year:  Option<u16>
        }
    
        #[allow(non_camel_case_types)]
        enum __DateFieldIdent {
            day,
            month,
            year
        }
    
        struct __DateProductVisitor {
            _marker: PhantomData<fn() -> __DateDePayload>
        }
    
        struct __DateSerPayload<'date> {
            day:   &'date u8,
            month: &'date u8,
            year:  &'date Option<u16>
        }
    
        struct __SumVisitor {
            _marker: PhantomData<fn() -> Birthday>
        }
    
        enum __Variant {
            Custom,
            Date
        }
    
        impl<'de> Deserialize<'de> for Birthday {
            fn deserialize<D: Deserializer<'de>>(
                deserializer: D
            ) -> Result<Self, D::Error> {
                deserializer.deserialize_sum(__SumVisitor {
                    _marker: PhantomData::<fn() -> Self>
                })
            }
    
            fn validate<D: Deserializer<'de>>(
                deserializer: D
            ) -> Result<(), D::Error> {
                deserializer.validate_sum(__SumVisitor {
                    _marker: PhantomData::<fn() -> Self>
                })
            }
        }
    
        impl Serialize for Birthday {
            fn serialize<S: Serializer>(
                &self,
                __serializer: S
            ) -> Result<S::Ok, S::Error> {
                match self {
                    Self::Custom(__variant) => __serializer
                        .serialize_variant::<String>(0, Some("Custom"), __variant),
                    Self::Date { day, month, year } => __serializer
                        .serialize_variant(1, Some("Date"), &__DateSerPayload {
                            day,
                            month,
                            year
                        })
                }
            }
        }
    
        impl SpacetimeType for Birthday {
            fn make_type<S: TypespaceBuilder>(
                __typespace: &mut S
            ) -> AlgebraicType {
                TypespaceBuilder::add(
                    __typespace,
                    TypeId::of::<Self>(),
                    Some("Birthday"),
                    |__typespace| {
                        AlgebraicType::sum::<[(&str, AlgebraicType); 2]>([
                            (
                                "Custom",
                                <String as SpacetimeType>::make_type(__typespace)
                            ),
                            (
                                "Date",
                                AlgebraicType::product::<
                                    [(Option<&str>, AlgebraicType); 3]
                                >([
                                    (
                                        Some("day"),
                                        <u8 as SpacetimeType>::make_type(
                                            __typespace
                                        )
                                    ),
                                    (
                                        Some("month"),
                                        <u8 as SpacetimeType>::make_type(
                                            __typespace
                                        )
                                    ),
                                    (
                                        Some("year"),
                                        <Option<u16> as SpacetimeType>::make_type(
                                            __typespace
                                        )
                                    )
                                ])
                            )
                        ])
                    }
                )
            }
        }
    
        impl<'de> Deserialize<'de> for __DateDePayload {
            fn deserialize<D: Deserializer<'de>>(
                deserializer: D
            ) -> Result<Self, D::Error> {
                deserializer.deserialize_product(__DateProductVisitor {
                    _marker: PhantomData::<fn() -> Self>
                })
            }
    
            fn validate<D: Deserializer<'de>>(
                deserializer: D
            ) -> Result<(), D::Error> {
                deserializer.validate_product(__DateProductVisitor {
                    _marker: PhantomData::<fn() -> Self>
                })
            }
        }
    
        impl FieldNameVisitor<'_> for __DateProductVisitor {
            type Output = __DateFieldIdent;
    
            fn field_names(&self) -> impl '_ + Iterator<Item = Option<&str>> {
                ["day", "month", "year"].into_iter().map(Some)
            }
    
            fn visit<E: DeError>(self, __name: &str) -> Result<Self::Output, E> {
                match __name {
                    "day" => Ok(__DateFieldIdent::day),
                    "month" => Ok(__DateFieldIdent::month),
                    "year" => Ok(__DateFieldIdent::year),
                    _ => Err(DeError::unknown_field_name(__name, &self))
                }
            }
    
            fn visit_seq(self, __index: usize) -> Self::Output {
                match __index {
                    0 => __DateFieldIdent::day,
                    1 => __DateFieldIdent::month,
                    2 => __DateFieldIdent::year,
                    _ => core::unreachable!()
                }
            }
        }
    
        impl<'de> ProductVisitor<'de> for __DateProductVisitor {
            type Output = __DateDePayload;
    
            fn product_len(&self) -> usize {
                3
            }
    
            fn product_name(&self) -> Option<&str> {
                Some("Date")
            }
    
            fn validate_named_product<A: NamedProductAccess<'de>>(
                self,
                mut __prod: A
            ) -> Result<(), A::Error> {
                let mut day = false;
                let mut month = false;
                let mut year = false;
                while let Some(__field) =
                    NamedProductAccess::get_field_ident(&mut __prod, Self {
                        _marker: PhantomData
                    })?
                {
                    match __field {
                        __DateFieldIdent::day => {
                            if day {
                                return Err(DeError::duplicate_field(
                                    0,
                                    Some("day"),
                                    &self
                                ));
                            }
                            NamedProductAccess::validate_field_value::<u8>(
                                &mut __prod
                            )?;
                            day = true;
                        }
                        __DateFieldIdent::month => {
                            if month {
                                return Err(DeError::duplicate_field(
                                    1,
                                    Some("month"),
                                    &self
                                ));
                            }
                            NamedProductAccess::validate_field_value::<u8>(
                                &mut __prod
                            )?;
                            month = true;
                        }
                        __DateFieldIdent::year => {
                            if year {
                                return Err(DeError::duplicate_field(
                                    2,
                                    Some("year"),
                                    &self
                                ));
                            }
                            NamedProductAccess::validate_field_value::<Option<u16>>(
                                &mut __prod
                            )?;
                            year = true;
                        }
                    }
                }
                if !day {
                    return Err(DeError::missing_field(0, Some("day"), &self));
                }
                if !month {
                    return Err(DeError::missing_field(1, Some("month"), &self));
                }
                if !year {
                    return Err(DeError::missing_field(2, Some("year"), &self));
                }
                Ok(())
            }
    
            fn validate_seq_product<A: SeqProductAccess<'de>>(
                self,
                mut tup: A
            ) -> Result<(), A::Error> {
                tup.validate_next_element::<u8>()?
                    .ok_or_else(|| DeError::invalid_product_length(0, &self))?;
                tup.validate_next_element::<u8>()?
                    .ok_or_else(|| DeError::invalid_product_length(1, &self))?;
                tup.validate_next_element::<Option<u16>>()?
                    .ok_or_else(|| DeError::invalid_product_length(2, &self))?;
                Ok(())
            }
    
            fn visit_named_product<A: NamedProductAccess<'de>>(
                self,
                mut __prod: A
            ) -> Result<Self::Output, A::Error> {
                let mut day = None;
                let mut month = None;
                let mut year = None;
                while let Some(__field) =
                    NamedProductAccess::get_field_ident(&mut __prod, Self {
                        _marker: PhantomData
                    })?
                {
                    match __field {
                        __DateFieldIdent::day => {
                            if day.is_some() {
                                return Err(DeError::duplicate_field(
                                    0,
                                    Some("day"),
                                    &self
                                ));
                            }
                            day = Some(NamedProductAccess::get_field_value::<u8>(
                                &mut __prod
                            )?);
                        }
                        __DateFieldIdent::month => {
                            if month.is_some() {
                                return Err(DeError::duplicate_field(
                                    1,
                                    Some("month"),
                                    &self
                                ));
                            }
                            month =
                                Some(NamedProductAccess::get_field_value::<u8>(
                                    &mut __prod
                                )?);
                        }
                        __DateFieldIdent::year => {
                            if year.is_some() {
                                return Err(DeError::duplicate_field(
                                    2,
                                    Some("year"),
                                    &self
                                ));
                            }
                            year = Some(NamedProductAccess::get_field_value::<
                                Option<u16>
                            >(&mut __prod)?);
                        }
                    }
                }
                Ok(__DateDePayload {
                    day:   day.ok_or_else(|| {
                        DeError::missing_field(0, Some("day"), &self)
                    })?,
                    month: month.ok_or_else(|| {
                        DeError::missing_field(1, Some("month"), &self)
                    })?,
                    year:  year.ok_or_else(|| {
                        DeError::missing_field(2, Some("year"), &self)
                    })?
                })
            }
    
            fn visit_seq_product<A: SeqProductAccess<'de>>(
                self,
                mut tup: A
            ) -> Result<Self::Output, A::Error> {
                Ok(__DateDePayload {
                    day:   tup
                        .next_element::<u8>()?
                        .ok_or_else(|| DeError::invalid_product_length(0, &self))?,
                    month: tup
                        .next_element::<u8>()?
                        .ok_or_else(|| DeError::invalid_product_length(1, &self))?,
                    year:  tup
                        .next_element::<Option<u16>>()?
                        .ok_or_else(|| DeError::invalid_product_length(2, &self))?
                })
            }
        }
    
        impl Serialize for __DateSerPayload<'_> {
            fn serialize<S: Serializer>(
                &self,
                __serializer: S
            ) -> Result<S::Ok, S::Error> {
                let mut __prod = __serializer.serialize_named_product(3)?;
                __prod.serialize_element::<u8>(Some("day"), self.day)?;
                __prod.serialize_element::<u8>(Some("month"), self.month)?;
                __prod.serialize_element::<Option<u16>>(Some("year"), self.year)?;
                __prod.end()
            }
        }
    
        impl<'de> SumVisitor<'de> for __SumVisitor {
            type Output = Birthday;
    
            fn sum_name(&self) -> Option<&str> {
                Some("Birthday")
            }
    
            fn validate_sum<A: SumAccess<'de>>(
                self,
                __data: A
            ) -> Result<(), A::Error> {
                let (__variant, __access) = __data.variant(self)?;
                match __variant {
                    __Variant::Custom => {
                        VariantAccess::validate::<String>(__access)?;
                    }
                    __Variant::Date => {
                        VariantAccess::validate::<__DateDePayload>(__access)?;
                    }
                }
                Ok(())
            }
    
            fn visit_sum<A: SumAccess<'de>>(
                self,
                __data: A
            ) -> Result<Self::Output, A::Error> {
                let (__variant, __access) = __data.variant(self)?;
                match __variant {
                    __Variant::Custom => Ok(Birthday::Custom(
                        VariantAccess::deserialize::<String>(__access)?
                    )),
                    __Variant::Date => {
                        let __payload = VariantAccess::deserialize::<
                            __DateDePayload
                        >(__access)?;
                        Ok(Birthday::Date {
                            day:   __payload.day,
                            month: __payload.month,
                            year:  __payload.year
                        })
                    }
                }
            }
        }
    
        impl VariantVisitor<'_> for __SumVisitor {
            type Output = __Variant;
    
            fn variant_names(&self) -> impl '_ + Iterator<Item = &str> {
                ["Custom", "Date"].into_iter()
            }
    
            fn visit_name<E: DeError>(
                self,
                __name: &str
            ) -> Result<Self::Output, E> {
                match __name {
                    "Custom" => Ok(__Variant::Custom),
                    "Date" => Ok(__Variant::Date),
                    _ => Err(DeError::unknown_variant_name(__name, &self))
                }
            }
    
            fn visit_tag<E: DeError>(self, __tag: u8) -> Result<Self::Output, E> {
                match __tag {
                    0 => Ok(__Variant::Custom),
                    1 => Ok(__Variant::Date),
                    _ => Err(DeError::unknown_variant_tag(__tag, &self))
                }
            }
        }
    };
    
    spacetimedb::spacetimedb_lib::__make_register_reftype!(Birthday, "Birthday");
    

Boost this Request

Reopen this request?

The request will return to its prior live status.

Mark as Duplicate

Pick the request that Full Algebraic Type Support is a duplicate of. Energy rolls up into that request and GitHub closes the linked issue as a duplicate with a timeline reference to the canonical.

Unmark Duplicate

Restore as an independent request. The linked GitHub issue will be reopened and its duplicate-of relationship cleared. The original timeline entries stay as history; the note below is posted as a follow-up comment.