specta

repository·main·Indexed 20 days ago

https://github.com/specta-rs/specta

A Rust library for exporting Rust types (structs, enums, etc.) to other programming languages to enable end-to-end type safety between Rust backends and client environments. It supports various exporters including TypeScript, Swift, OpenAPI 3.0, Java 17, Go, Python 3.13, Kotlin, JSON Schema, Zod, Valibot, C#, and ReScript.

Tokens
79.1K
Snippets
202
Records
370
Agent score
68%

What's inside specta

  1. Use `specta_serde::format_phases` for split serialization/deserialization shapes

    main

    The specta_serde::format_phases implementation handles cases where the wire format differs between serialization and deserialization.

    How it works:

    • It generates two distinct types: TypeName_Serialize and TypeName_Deserialize.
    • It generates a base TypeName which is a union of the two: TypeName_Serialize | TypeName_Deserialize.

    When to use:

    • When using directional Serde metadata like serialize_with, deserialize_with, from, into, or try_from.
    • When using the #[specta(type = specta_serde::Phased<SerializeTy, DeserializeTy>)] override.
    • Note: You should always prefer format_phases over format whenever possible to ensure type accuracy.
    use serde::{Deserialize, Serialize};
    use serde_with::{OneOrMany, serde_as};
    use specta::{Type, Types};
    use specta_typescript::Typescript;
    
    #[derive(Type, Serialize, Deserialize)]
    #[serde(untagged)]
    enum OneOrManyString {
        One(String),
        Many(Vec<String>),
    }
    
    #[serde_as]
    #[derive(Type, Serialize, Deserialize)]
    struct Filters {
        #[serde_as(as = "OneOrMany<_>")]
        #[specta(type = specta_serde::Phased<Vec<String>, OneOrManyString>)]
        tags: Vec<String>,
    }
    
    let types = Types::default().register::<Filters>();
    
    let output = Typescript::default()
        .export(&types, specta_serde::format_phases)
        .unwrap();
    
    assert!(output.contains("Filters_Serialize"));
    assert!(output.contains("Filters_Deserialize"));
    assert!(output.contains("OneOrManyString"));
  2. Support for complex Rust types

    main

    Specta Swift handles advanced Rust type features:

    • Complex Unions: Supports all enum variants including unit, tuple, named fields, and nested structs/enums.
    • Generics: Supports single and multiple generic type parameters (e.g., DatabaseResult<T, E>).
    • Recursive Types: Supports self-referencing and circular type definitions (e.g., a Shape enum containing a Vec<Shape>).
    • Duration: std::time::Duration is automatically converted to a RustDuration helper struct that includes a timeInterval property.
    // Example of a recursive, generic, and complex enum
    #[derive(Type)]
    enum Shape {
        None,
        Point(f64, f64),
        Circle { center: Point, radius: f64 },
        Complex { shapes: Vec<Shape>, metadata: Option<String> },
    }
    
    #[derive(Type)]
    enum DatabaseResult<T, E> {
        Ok { data: T, affected_rows: u64 },
        Err { error: E, query: String },
        ConnectionError { host: String, port: u16 },
    }
  3. Understand Java data model generation behavior

    main

    When exporting to Java, specta-java focuses on generating data models rather than serialization adapters. Note the following behaviors:

    • Field Names: Legal formatted field names are preserved.
    • Enums: String-based enums expose their wire value through a .value() method.
    • Escaping: Names that are invalid in Java are escaped deterministically. Ensure your application's serializer is configured to use these escaped names and enum values to maintain compatibility.
    • Tuples:
      • Rust tuples used as fields are converted into nested Java records.
      • Anonymous tuple positions that cannot be safely named are converted into Java Lists.
  4. Use `specta_serde::format` for unified types

    main

    The specta_serde::format implementation produces a single TypeScript type that represents both the serialized and deserialized shapes of a Rust type.

    When to use:

    • When Serde behavior is symmetric.
    • When you only need one TypeScript type per Rust struct/enum.

    Limitations:

    • It will error if you use asymmetric Serde attributes, such as #[serde(rename(serialize = "a", deserialize = "b"))], because it is unclear which name should be used for the single generated type.
    use specta::Types;
    use specta_typescript::Typescript;
    
    #[derive(specta::Type, serde::Serialize, serde::Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct User {
        user_id: u32,
    }
    
    let types = Types::default().register::<User>();
    
    let output = Typescript::default()
        .export(&types, specta_serde::format)
        .unwrap();
    
    assert!(output.contains("export type User"));
    assert!(output.contains("userId: number"));
  5. Configure specta-swift exporter settings

    main

    The configuration_options.rs example demonstrates how to customize the Swift code generation. You can control several aspects of the output, including:

    • Naming Conventions: Switch between PascalCase, camelCase, and snake_case.
    • Indentation: Configure indentation using spaces or tabs and specify the width.
    • Type Styles: Choose between protocol constraints or typealias for generic types, and decide between ? syntax or Optional<T> for optional types.
    • Metadata: Add custom headers and documentation.
    • Conformance: Add additional protocol conformance and configure Serde validation settings.
  6. Configure Java export file layouts

    main

    The specta-java exporter supports two primary ways of organizing generated files:

    1. Flat-file layout (Default): All generated types are nested inside a single wrapper class (e.g., Bindings). When using this layout, the output must be written to a file that matches the configured class name (e.g., Bindings.java).
    2. Individual files layout: Use Layout::Files with the export_to method to generate one public Java source file per type instead of nesting them.
  7. Preserve Rust documentation in Swift

    main

    Rust doc comments (///) are preserved and formatted as Swift documentation comments in the generated output.

    /// A comprehensive user account
    ///
    /// # Security Notes
    /// - The password field should never be logged
    #[derive(Type)]
    struct User {
        /// Unique identifier
        id: u32,
        name: String,
    }

    // Generates:

    /// A comprehensive user account
    ///
    /// # Security Notes
    /// - The password field should never be logged
    public struct User: Codable {
        /// Unique identifier
        public let id: UInt32
        public let name: String
    }
  8. Export Specta type collections to Java

    main

    Use specta-java to export your Rust types (defined via specta::Type) into Java 17 records, enums, and sealed interfaces. This allows you to maintain type-safe data models across the Rust/Java boundary.

    To export, use the Java exporter with export_to. You must register your types using Types::default().register::<T>() and specify a serialization format (e.g., specta_serde::Format).

    use specta::{Type, Types};
    use specta_java::Java;
    
    #[derive(Type)]
    struct User {
        name: String,
    }
    
    Java::default()
        .export_to(
            "Bindings.java",
            &Types::default().register::<User>(),
            specta_serde::Format,
        )?;
  9. Explore specta-swift type mappings and enum patterns

    main

    The specta-swift examples directory provides several specialized examples to help you understand how different Rust types are translated into Swift:

    • Fundamental Mappings: Use basic_types to see how primitives (i8, u32, f64, etc.), Option<T>, Vec<T>, and Tuples are converted.
    • Complex Enums: Use advanced_unions or string_enums to see how recursive types, generic enums, and string-based enums with Codable are handled.
    • Special Types: Use special_types to see how Duration is mapped to a RustDuration helper with a timeInterval property.
    • Documentation: Use comments_example to see how Rust doc comments are preserved as Swift-compatible documentation.
    • Full Showcase: Use comprehensive_demo for a realistic application pattern including user management, task tracking, and pagination.
  10. Install specta-rescript

    main

    To use specta-rescript to export Rust types to ReScript, add the following dependencies to your Cargo.toml file. Note that specta requires the derive feature enabled.

    [dependencies]
    specta = { version = "2.0.0-rc.26", features = ["derive"] }
    specta-rescript = "0.0.1"
    specta-serde = "0.0.13"
  11. Generate and type-check ReScript files

    main

    To use specta-rescript, you can generate .res files from Rust definitions and then verify them using the ReScript compiler.

    1. Generate .res files

    Run any specific example using cargo run with the -p specta-rescript package flag:

    cargo run -p specta-rescript --example rescript_simple_usage
    cargo run -p specta-rescript --example rescript_basic_types

    2. Type-check the generated code

    Navigate to the generated/ directory to install dependencies and run the ReScript compiler:

    cd examples/generated
    npm install
    npm run check

    Note: The lib/ directory produced by rescript build is ignored by git.

    cargo run -p specta-rescript --example rescript_simple_usage
    cd examples/generated
    npm install
    npm run check
  12. Use Specta Macros via the Specta crate

    main

    Do not add specta-macros as a direct dependency in your Cargo.toml. Instead, use the main specta crate, which re-exports the macros. This ensures version compatibility and simplifies dependency management.

    # Do NOT do this:
    # specta-macros = "..."
    
    # DO this:
    [dependencies]
    specta = "..."