typify

    Generate Rust and MoonBit types from JSON Schema (port of oxidecomputer/typify)

    json-schema
    codegen
    rust
    moonbit
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    1

    Dependencies

    #typify.mbt

    Generate Rust and MoonBit types from JSON Schema. This is a MoonBit port of oxidecomputer/typify, and it runs anywhere MoonBit's Wasm backend runs, with no Rust toolchain needed.

    • Rust output is identical to upstream typify's for the same schema and settings. It emits the same token stream as typify-impl, with the same type names, derives, serde attributes, builders and TryFrom validation.
    • MoonBit output mirrors the Rust types and adds JSON codecs that behave like the serde-derived Rust code:
      • the same inputs are accepted and rejected, with the same error messages;
      • accepted inputs decode to the same values;
      • values encode to byte-identical JSON.

      So a Rust service and a MoonBit program can share one schema and exchange JSON without surprises.

    #Command line

    moonx bobzhang/typify/cmd/typify schema.json # writes schema.rs moonx bobzhang/typify/cmd/typify schema.json --lang moonbit # writes schema.mbt moonx bobzhang/typify/cmd/typify schema.json -o - # to stdout

    The options are the same as cargo typify's, plus --lang:

    option
    -l, --lang <rust\|moonbit>language to generate (default rust)
    -b, --builder / -B, --no-builderbuilder-style interface for Rust structs (default on)
    -d, --additional-derive <derive>extra derive for every type
    -a, --additional-attr <attr>extra attribute for every type
    -o, --output <file>output file, - for stdout (default: input with .rs/.mbt)
    --crate <name@version>crates assumed available for x-rust-type
    --map-type <type>map type (default ::std::collections::HashMap)
    --unknown-crates <generate\|allow\|deny>policy for unknown x-rust-type crates

    Rust output is printed as a token stream, exactly as upstream typify produces it. cargo typify runs rustfmt on it; you can do the same:

    moonx bobzhang/typify/cmd/typify schema.json -o - | rustfmt --edition 2021 > schema.rs

    #Generated MoonBit

    The generated file is one MoonBit package. It depends on the runtime package:

    // moon.pkg of the package holding the generated file import { "bobzhang/typify/runtime" @typify_rt, }

    Every generated type T has the following:

    • T::from_json_str(text): decode JSON text, like serde_json::from_str.
    • value.to_json_string(): encode as compact JSON, like serde_json::to_string.
    • T::from_json(json) and value.to_json(): conversions to and from the builtin Json. These are lossy: duplicate keys and number spellings are not representable in Json.
    • T::deserialize(de) and value.serialize(): the underlying @typify_rt.Serde trait.
    • T::default() when the schema gives a default.
    • For string enums: to_string, parse and variant_index.

    For example, with this schema (upstream typify's README example):

    { "title": "Veggies", "type": "object", "properties": { "fruits": { "type": "array", "items": { "type": "string" } }, "vegetables": { "type": "array", "items": { "$ref": "#/definitions/veggie" } } }, "definitions": { "veggie": { "type": "object", "required": ["veggieName", "veggieLike"], "properties": { "veggieName": { "type": "string" }, "veggieLike": { "type": "boolean" } } } } }

    the generated MoonBit can be used like this:

    let veggies = Veggies::from_json_str(
    "{\"fruits\":[\"apple\"],\"vegetables\":[{\"veggieName\":\"leek\",\"veggieLike\":true}]}",
    )
    println(veggies.vegetables[0].veggie_name) // leek
    println(veggies.to_json_string()) // same JSON as serde_json::to_string

    #Type mapping

    JSON Schema / RustMoonBit
    bool, StringBool, String
    i8/i32, i16, i64Int, Int16, Int64
    u8, u16, u32, u64, NonZeroU*Byte, UInt16, UInt, UInt64 (non-zero is checked)
    f32, f64Float, Double (serialized with the shortest f32/f64 digits, as serde_json does)
    Option<T>, Box<T>T?, T
    Vec<T>, sets, [T; N], tuplesArray[T], Array[T], FixedArray[T], tuples
    HashMap<K, V> / BTreeMapMap[K, V] (BTreeMap output is key-sorted)
    serde_json::ValueJson
    uuid::Uuid, chrono::NaiveDate, chrono::DateTime<Utc>@typify_rt.Uuid, @typify_rt.NaiveDate, @typify_rt.DateTimeUtc
    std::net::{IpAddr, Ipv4Addr, Ipv6Addr}@typify_rt.IpAddr, Ipv4Addr, Ipv6Addr
    constrained strings, numbers and enumsnewtypes validated on construction and decoding

    Structs, enums (external, internal, adjacent and untagged tagging), flatten, deny_unknown_fields, defaults, positional (array) struct input and duplicate-key rejection all follow serde's rules.

    #Library

    let root = @schema.RootSchema::from_json_str(schema_text)
    let space = @typify.TypeSpace::new(@typify.TypeSpaceSettings::default())
    space.add_root_schema(root)
    let rust : String = @rust.to_stream(space).to_string()
    let moonbit : String = @moonbit.generate(space)

    TypeSpaceSettings supports the same builder methods as upstream typify: with_derive, with_attr, with_struct_builder, with_replacement, with_patch, with_conversion, with_crate, with_unknown_crates, with_map_type and with_type_mod.

    #How it is verified

    Every layer is checked differentially against the real Rust crates it ports. The oracles live in scripts/ and conformance/rust.

    • typify IR and Rust tokens:
      • upstream typify's type descriptions and token streams, compared token for token, for every upstream fixture (including the github and vega schemas);
      • thousands of random schemas covering tagged unions, merges, defaults, references and formats;
      • upstream's errors and panics.
    • serde behaviour of the generated MoonBit:
      • upstream's generated Rust is compiled with serde and fed the same instances, including duplicate keys, reordered keys, escapes, edge-case numbers, positional arrays and deep nesting;
      • output text and error messages must match exactly, across all upstream fixtures and hundreds of random schemas.
    • Building blocks:
      • serde_json parsing, errors and number formatting (including zmij's f32/f64 output);
      • regress regular expressions;
      • schemars schema decoding;
      • heck case conversion;
      • Unicode identifier tables;
      • semver;
      • the uuid, chrono and std::net parsers and printers.

    #License

    Apache-2.0, like upstream typify. The upstream fixtures in tests/upstream are copied from oxidecomputer/typify.

    TypifyError

    pub(all) suberror TypifyError {
    BadValue(String,
    Value
    )
    InvalidTypeId
    InvalidValue
    InvalidSchema(type_name~ : String?, reason~ : String)
    Panic(String)
    } derive(
    Debug
    )

    Errors from type generation (upstream typify_impl::Error).

    Panic stands for the places where upstream typify panics (panic!, todo!, unimplemented!, failed unwraps). It is kept distinct so that code which deliberately ignores ordinary errors (upstream's .ok()) does not swallow what upstream would treat as a crash.
    impl Show for TypifyError

    Case

    pub(all) enum Case {
    Pascal
    Snake
    }

    CrateVers

    pub(all) enum CrateVers {
    Version(
    Version
    )
    Any
    Never
    } derive(
    Debug
    )

    A crate version to accept for x-rust-type.

    CrateVers::parse

    fn CrateVers::parse(s : String) -> CrateVers?

    Parse ! (never), * (any) or a semver version.

    DefaultImpl

    pub(all) enum DefaultImpl {
    Boolean
    I64
    U64
    NZU64
    } derive(Compare, Eq,
    Debug
    )

    Shared default-value helper functions (defaults::default_*).

    DefaultImpl::fn_name

    fn DefaultImpl::fn_name(self : DefaultImpl) -> String

    DefaultKind

    pub(all) enum DefaultKind {
    Intrinsic
    Specific
    Generic(DefaultImpl)
    } derive(
    Debug
    )

    How a default value can be produced.

    EnumTagType

    pub(all) enum EnumTagType {
    External
    Internal(tag~ : String)
    Adjacent(tag~ : String, content~ : String)
    Untagged
    } derive(Eq,
    Debug
    )

    Name

    pub(all) enum Name {
    Required(String)
    Suggested(String)
    Unknown
    } derive(Eq,
    Debug
    )

    A name for a type being converted.

    Name::append

    fn Name::append(self : Name, s : String) -> Name

    Derive a suggested sub-name: prefix_s.

    Name::into_option

    fn Name::into_option(self : Name) -> String?

    RefKey

    pub(all) enum RefKey {
    Root
    Def(String)
    } derive(Eq, Hash,
    Debug
    )

    A key for referenceable schemas: the root or a named definition.

    StructProperty

    pub(all) struct StructProperty {
    name : String
    rename : StructPropertyRename
    state : StructPropertyState
    description : String?
    type_id : TypeId
    } derive(Eq,
    Debug
    )

    StructPropertyRename

    pub(all) enum StructPropertyRename {
    NoRename
    Rename(String)
    Flatten
    } derive(Eq,
    Debug
    )

    StructPropertyState

    pub(all) enum StructPropertyState {
    Required
    Optional
    Default(
    Value
    )
    } derive(Eq,
    Debug
    )

    TypeEntry

    TypeEntry::describe

    fn TypeEntry::describe(self : TypeEntry) -> String

    A short textual description of a type, for debugging (upstream TypeEntry::describe).

    TypeEntry::from_details

    fn TypeEntry::from_details(details : TypeEntryDetails) -> TypeEntry

    TypeEntry::has_impl

    fn TypeEntry::has_impl(self : TypeEntry, space : TypeSpace, impl_name : TypeSpaceImpl) -> Bool raise TypifyError

    Whether the type is known to implement impl_name.

    TypeEntry::name

    fn TypeEntry::name(self : TypeEntry) -> String?

    The name of a named (enum, struct or newtype) type.

    TypeEntry::new_float

    fn TypeEntry::new_float(type_name : String) -> TypeEntry

    TypeEntry::new_integer

    fn TypeEntry::new_integer(type_name : String) -> TypeEntry

    TypeEntry::new_native

    fn TypeEntry::new_native(type_name : String, impls : Array[TypeSpaceImpl]) -> TypeEntry

    TypeEntry::new_native_params

    fn TypeEntry::new_native_params(type_name : String, parameters : Array[TypeId]) -> TypeEntry

    TypeEntry::validate_value

    fn TypeEntry::validate_value(self : TypeEntry, space : TypeSpace, value :
    Value
    ) -> DefaultKind raise TypifyError

    Check that a value is a valid instance of this type, and classify how a default of that value can be produced.

    TypeEntryDetails

    pub(all) enum TypeEntryDetails {
    Enum(TypeEntryEnum)
    Struct(TypeEntryStruct)
    Newtype(TypeEntryNewtype)
    Native(TypeEntryNative)
    Option(TypeId)
    Box(TypeId)
    Vec(TypeId)
    Map(TypeId, TypeId)
    Set(TypeId)
    Array(TypeId, Int)
    Tuple(Array[TypeId])
    Unit
    Boolean
    Integer(String)
    Float(String)
    String
    JsonValue
    Reference(TypeId)
    } derive(Eq,
    Debug
    )

    TypeEntryEnum

    pub(all) struct TypeEntryEnum {
    name : String
    rename : String?
    description : String?
    default :
    Value
    ?
    tag_type : EnumTagType
    variants : Array[Variant]
    deny_unknown_fields : Bool
    bespoke_impls : Array[TypeEntryEnumImpl]
    schema :
    Schema

    } derive(Eq,
    Debug
    )

    TypeEntryEnumImpl

    pub(all) enum TypeEntryEnumImpl {
    AllSimpleVariants
    UntaggedFromStr
    UntaggedDisplay
    UntaggedFromStringIrrefutable
    } derive(Compare, Eq,
    Debug
    )

    TypeEntryNative

    pub(all) struct TypeEntryNative {
    type_name : String
    impls : Array[TypeSpaceImpl]
    parameters : Array[TypeId]
    } derive(Eq,
    Debug
    )

    TypeEntryNative::name_match

    fn TypeEntryNative::name_match(self : TypeEntryNative, type_name : Name) -> Bool

    Whether a native type's last path segment matches a required type name.

    TypeEntryNewtype

    pub(all) struct TypeEntryNewtype {
    name : String
    rename : String?
    description : String?
    default :
    Value
    ?
    type_id : TypeId
    constraints : TypeEntryNewtypeConstraints
    schema :
    Schema

    } derive(Eq,
    Debug
    )

    TypeEntryNewtypeConstraints

    pub(all) enum TypeEntryNewtypeConstraints {
    NoConstraints
    EnumValue(Array[
    Value
    ])
    DenyValue(Array[
    Value
    ])
    String(max_length~ : UInt?, min_length~ : UInt?, pattern~ : String?)
    } derive(Eq,
    Debug
    )

    TypeEntryStruct

    pub(all) struct TypeEntryStruct {
    name : String
    rename : String?
    description : String?
    default :
    Value
    ?
    properties : Array[StructProperty]
    deny_unknown_fields : Bool
    schema :
    Schema

    } derive(Eq,
    Debug
    )

    TypeId

    pub(all) struct TypeId(Int) derive(Compare, Eq, Hash,
    Debug
    )

    Identifier of a type within a TypeSpace.
    impl Show for TypeId

    TypeSpace

    pub struct TypeSpace {
    id_to_entry :
    SortedMap
    [TypeId, TypeEntry]
    uses_chrono : Bool
    uses_uuid : Bool
    uses_serde_json : Bool
    uses_regress : Bool
    settings : TypeSpaceSettings
    defaults : Array[DefaultImpl]
    // private fields
    }

    A collection of types generated from JSON Schema (upstream TypeSpace).

    TypeSpace::add_ref_types

    fn TypeSpace::add_ref_types(self : TypeSpace, type_defs : Array[(String,
    Schema
    )]) -> Unit raise TypifyError

    Add named definitions that may reference each other. Each call must be self-contained.

    TypeSpace::add_root_schema

    Add all definitions of a root schema, plus the root itself if it has a title. Returns the root's id in that case.

    TypeSpace::add_type

    Add a type and return its id.

    TypeSpace::add_type_with_name

    fn TypeSpace::add_type_with_name(self : TypeSpace, schema :
    Schema
    , name_hint : String?) -> TypeId raise TypifyError

    Add a type with a name hint and return its id.

    TypeSpace::entries

    fn TypeSpace::entries(self : TypeSpace) -> Array[(TypeId, TypeEntry)]

    All type entries in id order.

    TypeSpace::entry

    fn TypeSpace::entry(self : TypeSpace, id : TypeId) -> TypeEntry raise TypifyError

    Look up a type entry.

    TypeSpace::new

    fn TypeSpace::new(settings : TypeSpaceSettings) -> TypeSpace

    Create a type space with the given settings.

    TypeSpace::type_count

    fn TypeSpace::type_count(self : TypeSpace) -> Int

    The number of types in the space.

    TypeSpaceImpl

    pub(all) enum TypeSpaceImpl {
    FromStr
    FromStringIrrefutable
    Display
    Default
    } derive(Compare, Eq, Hash,
    Debug
    )

    Traits a type may be known to implement.

    TypeSpaceImpl::parse

    fn TypeSpaceImpl::parse(s : String) -> TypeSpaceImpl raise TypifyError

    Parse an impl name as accepted in settings ("FromStr", "Display", "Default").

    TypeSpacePatch

    pub struct TypeSpacePatch {
    rename : String?
    derives : Array[String]
    attrs : Array[String]
    } derive(
    Debug
    )

    Modifications applied to a generated type with a given name.

    TypeSpacePatch::new

    TypeSpacePatch::with_attr

    fn TypeSpacePatch::with_attr(self : TypeSpacePatch, attr : String) -> TypeSpacePatch

    TypeSpacePatch::with_derive

    fn TypeSpacePatch::with_derive(self : TypeSpacePatch, derive_name : String) -> TypeSpacePatch

    TypeSpacePatch::with_rename

    fn TypeSpacePatch::with_rename(self : TypeSpacePatch, rename : String) -> TypeSpacePatch

    TypeSpaceReplace

    pub(all) struct TypeSpaceReplace {
    replace_type : String
    impls : Array[TypeSpaceImpl]
    } derive(
    Debug
    )

    A replacement of a named definition by an existing type.

    TypeSpaceSettings

    pub struct TypeSpaceSettings {
    type_mod : String?
    extra_derives : Array[String]
    extra_attrs : Array[String]
    struct_builder : Bool
    unknown_crates : UnknownPolicy
    map_type : String
    patch : Map[String, TypeSpacePatch]
    replace : Map[String, TypeSpaceReplace]
    // private fields
    }

    Settings that alter type generation (upstream TypeSpaceSettings).

    TypeSpaceSettings::default

    TypeSpaceSettings::with_attr

    fn TypeSpaceSettings::with_attr(self : TypeSpaceSettings, attr : String) -> TypeSpaceSettings

    Add an attribute applied to all defined types.

    TypeSpaceSettings::with_conversion

    Map schemas exactly equal to schema (ignoring metadata) to a named type (first one wins).

    TypeSpaceSettings::with_crate

    fn TypeSpaceSettings::with_crate(self : TypeSpaceSettings, crate_name : String, version : CrateVers, rename? : String) -> TypeSpaceSettings

    Accept x-rust-type types from crate_name at version, optionally renaming the crate in paths.

    TypeSpaceSettings::with_derive

    fn TypeSpaceSettings::with_derive(self : TypeSpaceSettings, derive_name : String) -> TypeSpaceSettings

    Add a derive macro applied to all defined types.

    TypeSpaceSettings::with_map_type

    fn TypeSpaceSettings::with_map_type(self : TypeSpaceSettings, map_type : String) -> TypeSpaceSettings

    The map-like type used in generated code.

    TypeSpaceSettings::with_patch

    fn TypeSpaceSettings::with_patch(self : TypeSpaceSettings, type_name : String, patch : TypeSpacePatch) -> TypeSpaceSettings

    Patch a generated type by name (last one wins).

    TypeSpaceSettings::with_replacement

    fn TypeSpaceSettings::with_replacement(self : TypeSpaceSettings, type_name : String, replace_type : String, impls : Array[TypeSpaceImpl]) -> TypeSpaceSettings

    Replace a named definition with an existing type (last one wins).

    TypeSpaceSettings::with_struct_builder

    fn TypeSpaceSettings::with_struct_builder(self : TypeSpaceSettings, struct_builder : Bool) -> TypeSpaceSettings

    Generate builder types for structs.

    TypeSpaceSettings::with_type_mod

    fn TypeSpaceSettings::with_type_mod(self : TypeSpaceSettings, type_mod : String) -> TypeSpaceSettings

    Path prefix for types defined in the type space.

    TypeSpaceSettings::with_unknown_crates

    fn TypeSpaceSettings::with_unknown_crates(self : TypeSpaceSettings, policy : UnknownPolicy) -> TypeSpaceSettings

    UnknownPolicy

    pub(all) enum UnknownPolicy {
    Generate
    Allow
    Deny
    } derive(Eq,
    Debug
    )

    Policy for x-rust-type extensions naming crates not configured with with_crate.

    Variant

    pub(all) struct Variant {
    raw_name : String
    ident_name : String?
    description : String?
    details : VariantDetails
    } derive(Eq,
    Debug
    )

    Variant::new

    fn Variant::new(raw_name : String, description : String?, details : VariantDetails) -> Variant

    VariantDetails

    pub(all) enum VariantDetails {
    Simple
    Item(TypeId)
    Tuple(Array[TypeId])
    Struct(Array[StructProperty])
    } derive(Eq,
    Debug
    )

    accept_as_ident

    fn accept_as_ident(ident : String) -> Bool

    Whether ident may be used as a Rust identifier as-is (upstream accept_as_ident): false for keywords, which get a trailing _.

    recase

    fn recase(input : String, case : Case) -> (String, String?)

    Sanitize and report the original name if it changed.

    sanitize

    fn sanitize(input : String, case : Case) -> String

    Turn arbitrary text into a Rust identifier in the given case (upstream sanitize).

    std_num_nonzero_prefix

    let std_num_nonzero_prefix : String

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io