Serializer: complete implementation

Looking back at our original desired flow:

structureserializerpropertylistString

We can now see this reflected directly in the types of our serializer:

Serializer[Root]Serializer[Struct[S]]Serializer[Property[S]]StringSerializer[List[S]]finishstructserializestructfinishlistfinishstructserializestringorstructserializepropertyfinishfinishstructserializelistserializelistorstringorfinishlist

The code for the full implementation of the Serializer and all its states can be found in this Rust playground.

  • This pattern isn’t a silver bullet. It still allows issues like:

    • Empty or invalid property names (which can be fixed using the newtype pattern)
    • Duplicate property names (which could be tracked in Struct<S> and handled via Result)
  • If validation failures occur, we can also change method signatures to return a Result, allowing recovery:

    #![allow(unused)]
    fn main() {
    struct PropertySerializeError<S> {
        kind: PropertyError,
        serializer: Serializer<Struct<S>>,
    }
    
    impl<S> Serializer<Struct<S>> {
        fn serialize_property(
            self,
            name: &str,
        ) -> Result<Serializer<Property<Struct<S>>>, PropertySerializeError<S>> {
            /* ... */
        }
    }
    }
  • While this API is powerful, it’s not always ergonomic. Production serializers typically favor simpler APIs and reserve the typestate pattern for enforcing critical invariants.

  • One excellent real-world example is rustls::ClientConfig, which uses typestate with generics to guide the user through safe and correct configuration steps.