StructTypes.jl

This guide provides documentation around the StructTypes.StructType trait for Julia objects and its associated functions. This package was born from a desire to make working with, and especially constructing, Julia objects more programmatic and customizable. This allows powerful workflows when doing generic object transformations and serialization.

If anything isn't clear or you find bugs, don't hesitate to open a new issue, even just for a question, or come chat with us on the #data slack channel with questions, concerns, or clarifications.

StructTypes.StructType

In general, custom Julia types tend to be one of: 1) "data types", 2) "interface types" or sometimes 3) "custom types" or 4) "abstract types" with a known set of concrete subtypes. Data types tend to be "collection of fields" kind of types; fields are generally public and directly accessible, they might also be made to model "objects" in the object-oriented sense. In any case, the type is "nominal" in the sense that it's "made up" of the fields it has, sometimes even if just for making it more convenient to pass them around together in functions.

Interface types, on the other hand, are characterized by private fields; they contain optimized representations "under the hood" to provide various features/functionality and are useful via interface methods implemented: iteration, getindex, accessor methods, etc. Many package-provided libraries or Base-provided structures are like this: Dict, Array, Socket, etc. For these types, their underlying fields are mostly cryptic and provide little value to users directly, and are often explictly documented as being implementation details and not to be relied upon directly under warning of breakage.

What does all this have to do with the StructTypes.StructType trait? A lot! There's often a desire to programmatically access the "public" names and values of an object, whether it's a data, interface, custom or abstract type. For data types, this means each direct field name and value. For interface types, this means having an API to get the names and values (ignoring direct fields). Similarly for programmatic construction, we need to specify how to construct the Julia structure given an arbitrary set of key-value pairs.

For "custom" types, this is kind of a catchall for those types that don't really fit in the "data" or "interface" buckets; like wrapper types. You don't really care about the wrapper type itself but about the type it wraps with a few modifications.

For abstract types, it can be useful to "bundle" the behavior of concrete subtypes under a single abstract type; and when serializing/deserializing, an extra key-value pair is added to encode the true concrete type.

Each of these 4 kinds of struct type categories will be now be detailed.

DataTypes

You'll remember that "data types" are Julia structs that are "made up" of their fields. In the object-oriented world, this would be characterized by marking a field as public. A quick example is:

struct Vehicle
    make::String
    model::String
    year::Int
end

In this case, our Vehicle type is entirely "made up" by its fields, make, model, and year.

There are three ways to define the StructTypes.StructType of these kinds of objects:

StructTypes.StructType(::Type{MyType}) = StructTypes.Struct() # an alias for StructTypes.UnorderedStruct()
# or
StructTypes.StructType(::Type{MyType}) = StructTypes.Mutable()
# or
StructTypes.StructType(::Type{MyType}) = StructTypes.OrderedStruct()

StructTypes.Struct

StructTypes.StructType
StructTypes.StructType(::Type{T}) = StructTypes.Struct()
StructTypes.StructType(::Type{T}) = StructTypes.UnorderedStruct()
StructTypes.StructType(::Type{T}) = StructTypes.OrderedStruct()

Signal that T is an immutable type who's fields should be used directly when serializing/deserializing. If a type is defined as StructTypes.Struct, it defaults to StructTypes.UnorderedStruct, which means its fields are allowed to be serialized/deserialized in any order, as opposed to StructTypes.OrderedStruct which signals that serialization/deserialization must occur in its defined field order exclusively. This can enable optimizations when an order can be guaranteed, but care must be taken to ensure any serialization formats can properly guarantee the order (for example, the JSON specification doesn't explicitly require ordered fields for "objects", though most implementations have a way to support this).

For StructTypes.UnorderedStruct, if a field is missing from the serialization, nothing should be passed to the StructTypes.construct method.

For example, when deserializing a Struct.OrderedStruct, parsed input fields are passed directly, in input order to the T constructor, like T(field1, field2, field3). This means that field names may be ignored when deserializing; fields are directly passed to T in the order they're encountered.

Another example, for reading a StructTypes.OrderedStruct() from a JSON string input, each key-value pair is read in the order it is encountered in the JSON input, the keys are ignored, and the values are directly passed to the type at the end of the object parsing like T(val1, val2, val3). Yes, the JSON specification says that Objects are specifically un-ordered collections of key-value pairs, but the truth is that many JSON libraries provide ways to maintain JSON Object key-value pair order when reading/writing. Because of the minimal processing done while parsing, and the "trusting" that the Julia type constructor will be able to handle fields being present, missing, or even extra fields that should be ignored, this is the fastest possible method for mapping a JSON input to a Julia structure. If your workflow interacts with non-Julia APIs for sending/receiving JSON, you should take care to test and confirm the use of StructTypes.OrderedStruct() in the cases mentioned above: what if a field is missing when parsing? what if the key-value pairs are out of order? what if there extra fields get included that weren't anticipated? If your workflow is questionable on these points, or it would be too difficult to account for these scenarios in your type constructor, it would be better to consider the StructTypes.UnorderedStruct or StructTypes.Mutable() options.

struct CoolType
    val1::Int
    val2::Int
    val3::String
end

StructTypes.StructType(::Type{CoolType}) = StructTypes.OrderedStruct()

# JSON3 package as example
@assert JSON3.read("{"val1": 1, "val2": 2, "val3": 3}", CoolType) == CoolType(1, 2, "3")
# note how `val2` field is first, then `val1`, but fields are passed *in-order* to `CoolType` constructor; BE CAREFUL!
@assert JSON3.read("{"val2": 2, "val1": 1, "val3": 3}", CoolType) == CoolType(2, 1, "3")
# if we instead define `Struct`, which defaults to `StructTypes.UnorderedStruct`, then the above example works
StructTypes.StructType(::Type{CoolType}) = StructTypes.Struct()
@assert JSON3.read("{"val2": 2, "val1": 1, "val3": 3}", CoolType) == CoolType(1, 2, "3")
source

StructTypes.Mutable

StructTypes.MutableType
StructTypes.StructType(::Type{T}) = StructTypes.Mutable()

Signal that T is a mutable struct with an empty constructor for serializing/deserializing. Though slightly less performant than StructTypes.Struct, Mutable is a much more robust method for mapping Julia struct fields for serialization. This technique requires your Julia type to be defined, at a minimum, like:

mutable struct T
    field1
    field2
    field3
    # etc.

    T() = new()
end

Note specifically that we're defining a mutable struct to allow field mutation, and providing a T() = new() inner constructor which constructs an "empty" T where isbitstype fields will be randomly initialized, and reference fields will be #undef. (Note that the inner constructor doesn't need to be exactly this, but at least needs to be callable like T(). If certain fields need to be intialized or zeroed out for security, then this should be accounted for in the inner constructor). For these mutable types, the type will first be initialized like T(), then serialization will take each key-value input pair, setting the field as the key is encountered, and converting the value to the appropriate field value. This flow has the nice properties of: allowing object construction success even if fields are missing in the input, and if "extra" fields exist in the input that aren't apart of the Julia struct's fields, they will automatically be ignored. This allows for maximum robustness when mapping Julia types to arbitrary data foramts that may be generated via web services, databases, other language libraries, etc.

There are a few additional helper methods that can be utilized by StructTypes.Mutable() types to hand-tune field reading/writing behavior:

  • StructTypes.names(::Type{T}) = ((:juliafield1, :serializedfield1), (:juliafield2, :serializedfield2)): provides a mapping of Julia field name to expected serialized object key name. This affects both serializing and deserializing. When deserializing the serializedfield1 key, the juliafield1 field of T will be set. When serializing the juliafield2 field of T, the output key will be serializedfield2. Field name mappings are provided as a Tuple of Tuple{Symbol, Symbol}s, i.e. each field mapping is a Julia field name Symbol (first) and serialized field name Symbol (second).

  • StructTypes.excludes(::Type{T}) = (:field1, :field2): specify fields of T to ignore when serializing and deserializing, provided as a Tuple of Symbols. When deserializing, if field1 is encountered as an input key, it's value will be read, but the field will not be set in T. When serializing, field1 will be skipped when serializing out T fields as key-value pairs.

  • StructTypes.omitempties(::Type{T}) = (:field1, :field2): specify fields of T that shouldn't be serialized if they are "empty", provided as a Tuple of Symbols. This only affects serializing. If a field is a collection (AbstractDict, AbstractArray, etc.) and isempty(x) === true, then it will not be serialized. If a field is #undef, it will not be serialized. If a field is nothing, it will not be serialized. To apply this to all fields of T, set StructTypes.omitempties(::Type{T}) = true. You can customize this behavior. For example, by default, missing is not considered to be "empty". If you want missing to be considered "empty" when serializing your type MyType, simply define:

@inline StructTypes.isempty(::Type{T}, ::Missing) where {T <: MyType} = true
  • StructTypes.keywordargs(::Type{T}) = (field1=(dateformat=dateformat"mm/dd/yyyy",), field2=(dateformat=dateformat"HH MM SS",)): provide keyword arguments for fields of type T that should be passed to functions that set values for this field. Define StructTypes.keywordargs as a NamedTuple of NamedTuples.
source

Support functions for StructTypes.DataTypes:

StructTypes.namesFunction
StructTypes.names(::Type{T}) = ((:juliafield1, :serializedfield1), (:juliafield2, :serializedfield2))

Provides a mapping of Julia field name to expected serialized object key name. This affects both reading and writing. When reading the serializedfield1 key, the juliafield1 field of T will be set. When writing the juliafield2 field of T, the output key will be serializedfield2.

source
StructTypes.excludesFunction
StructTypes.excludes(::Type{T}) = (:field1, :field2)

Specify for a StructTypes.Mutable StructType the fields, given as a Tuple of Symbols, that should be ignored when deserializing, and excluded from serializing.

source
StructTypes.omitemptiesFunction
StructTypes.omitempties(::Type{T}) = (:field1, :field2)
StructTypes.omitempties(::Type{T}) = true

Specify for a StructTypes.Mutable StructType the fields, given as a Tuple of Symbols, that should not be serialized if they're considered "empty".

If a field is a collection (AbstractDict, AbstractArray, etc.) and isempty(x) === true, then it will not be serialized. If a field is #undef, it will not be serialized. If a field is nothing, it will not be serialized. To apply this to all fields of T, set StructTypes.omitempties(::Type{T}) = true. You can customize this behavior. For example, by default, missing is not considered to be "empty". If you want missing to be considered "empty" when serializing your type MyType, simply define:

@inline StructTypes.isempty(::Type{T}, ::Missing) where {T <: MyType} = true
source
StructTypes.keywordargsFunction
StructTypes.keywordargs(::Type{MyType}) = (field1=(dateformat=dateformat"mm/dd/yyyy",), field2=(dateformat=dateformat"HH MM SS",))

Specify for a StructTypes.Mutable the keyword arguments by field, given as a NamedTuple of NamedTuples, that should be passed to the StructTypes.construct method when deserializing MyType. This essentially allows defining specific keyword arguments you'd like to be passed for each field in your struct. Note that keyword arguments can be passed when reading, like JSON3.read(source, MyType; dateformat=...) and they will be passed down to each StructTypes.construct method. StructTypes.keywordargs just allows the defining of specific keyword arguments per field.

source
StructTypes.idpropertyFunction
StructTypes.idproperty(::Type{MyType}) = :id

Specify which field of a type uniquely identifies it. The unique identifier field name is given as a Symbol. Useful in database applications where the id field can be used to distinguish separate objects.

source
StructTypes.fieldprefixFunction
StructTypes.fieldprefix(::Type{MyType}, field::Symbol) = :field_

When interacting with database tables and other strictly 2D data formats, objects with aggregate fields must be flattened into a single set of column names. When deserializing a set of columns into an object with aggregate fields, a field type's fieldprefix signals that column names beginning with, in the example above, :field_, should be collected together when constructing the field field of MyType. Note the default definition is StructTypes.fieldprefix(T, nm) = Symbol(nm, :_).

Here's a more concrete, albeit contrived, example:

struct Spouse
    id::Int
    name::String
end

StructTypes.StructType(::Type{Spouse}) = StructTypes.Struct()

struct Person
    id::Int
    name::String
    spouse::Spouse
end

StructTypes.StructType(::Type{Person}) = StructTypes.Struct()
StructTypes.fieldprefix(::Type{Person}, field::Symbol) = field == :spouse ? :spouse_ : :_

Here we have two structs, Spouse and Person, and a Person has a spouse::Spouse. The database tables to represent these entities might look like:

CREATE TABLE spouse (id INT, name VARCHAR);
CREATE TABLE person (id INT, name VARCHAR, spouse_id INT);

If we want to leverage a package like Strapping.jl to automatically handle the object construction for us, we could write a get query like the following to ensure a full Person with field spouse::Spouse can be constructed:

getPerson(id::Int) = Strapping.construct(Person, DBInterface.execute(db,
    """
        SELECT person.id as id, person.name as name, spouse.id as spouse_id, spouse.name as spouse_name
        FROM person
        LEFT JOIN spouse ON person.spouse_id = spouse.id
        WHERE person.id = $id
    """))

This works because the column names in the resultset of this query are "id, name, spouse_id, spouse_name"; because we defined StructTypes.fieldprefix for Person, Strapping.jl knows that each column starting with "spouse_" should be used in constructing the Spouse field of Person.

source

Interface Types

For interface types, we don't want the internal fields of a type exposed, so an alternative API is to define the closest "basic" type that our custom type should map to. This is done by choosing one of the following definitions:

StructTypes.StructType(::Type{MyType}) = StructTypes.DictType()
StructTypes.StructType(::Type{MyType}) = StructTypes.ArrayType()
StructTypes.StructType(::Type{MyType}) = StructTypes.StringType()
StructTypes.StructType(::Type{MyType}) = StructTypes.NumberType()
StructTypes.StructType(::Type{MyType}) = StructTypes.BoolType()
StructTypes.StructType(::Type{MyType}) = StructTypes.NullType()

Now we'll walk through each of these and what it means to map my custom Julia type to an interface type.

StructTypes.DictType

StructTypes.DictTypeType
StructTypes.StructType(::Type{T}) = StructTypes.DictType()

Declare that T should map to a dict-like object of unordered key-value pairs, where keys are Symbol, String, or Int64, and values are any other type (or Any).

Types already declared as StructTypes.DictType() include:

  • Any subtype of AbstractDict
  • Any NamedTuple type
  • The Pair type

So if your type subtypes AbstractDict and implements its interface, then it will inherit the DictType definition and serializing/deserializing should work automatically.

Otherwise, the interface to satisfy StructTypes.DictType() for deserializing is:

  • T(x::Dict{Symbol, Any}): implement a constructor that takes a Dict{Symbol, Any} of input key-value pairs
  • StructTypes.construct(::Type{T}, x::Dict; kw...): alternatively, you may overload the StructTypes.construct method for your type if defining a constructor is undesirable (or would cause other clashes or ambiguities)

The interface to satisfy for serializing is:

  • pairs(x): implement the pairs iteration function (from Base) to iterate key-value pairs to be serialized
  • StructTypes.keyvaluepairs(x::T): alternatively, you can overload the StructTypes.keyvaluepairs function if overloading pairs isn't possible for whatever reason
source

StructTypes.ArrayType

StructTypes.ArrayTypeType
StructTypes.StructType(::Type{T}) = StructTypes.ArrayType()

Declare that T should map to an array of ordered elements, homogenous or otherwise.

Types already declared as StructTypes.ArrayType() include:

  • Any subtype of AbstractArray
  • Any subtype of AbstractSet
  • Any Tuple type

So if your type already subtypes these and satifies their interface, things should just work.

Otherwise, the interface to satisfy StructTypes.ArrayType() for deserializing is:

  • T(x::Vector): implement a constructor that takes a Vector argument of values and constructs a T
  • StructTypes.construct(::Type{T}, x::Vector; kw...): alternatively, you may overload the StructTypes.construct method for your type if defining a constructor isn't possible
  • Optional: Base.IteratorEltype(::Type{T}) = Base.HasEltype() and Base.eltype(x::T): this can be used to signal that elements for your type are expected to be a homogenous type

The interface to satisfy for serializing is:

  • iterate(x::T): just iteration over each element is required; note if you subtype AbstractArray and define getindex(x::T, i::Int), then iteration is inherited for your type
source

StructTypes.StringType

StructTypes.StringTypeType
StructTypes.StructType(::Type{T}) = StructTypes.StringType()

Declare that T should map to a string value.

Types already declared as StructTypes.StringType() include:

  • Any subtype of AbstractString
  • The Symbol type
  • Any subtype of Enum (values are written with their symbolic name)
  • Any subtype of AbstractChar
  • The UUID type
  • Any Dates.TimeType subtype (Date, DateTime, Time, etc.)

So if your type is an AbstractString or Enum, then things should already work.

Otherwise, the interface to satisfy StructTypes.StringType() for deserializing is:

  • T(x::String): define a constructor for your type that takes a single String argument
  • StructTypes.construct(::Type{T}, x::String; kw...): alternatively, you may overload StructTypes.construct for your type
  • StructTypes.construct(::Type{T}, ptr::Ptr{UInt8}, len::Int; kw...): another option is to overload StructTypes.construct with pointer and length arguments, if it's possible for your custom type to take advantage of avoiding the full string materialization; note that your type should implement both StructTypes.construct methods, since direct pointer/length deserialization may not be possible for some inputs

The interface to satisfy for serializing is:

  • Base.string(x::T): overload Base.string for your type to return a "stringified" value, or more specifically, that returns an AbstractString, and should implement ncodeunits(x) and codeunit(x, i).
source

StructTypes.NumberType

StructTypes.NumberTypeType
StructTypes.StructType(::Type{T}) = StructTypes.NumberType()

Declare that T should map to a number value.

Types already declared as StructTypes.NumberType() include:

  • Any subtype of Signed
  • Any subtype of Unsigned
  • Any subtype of AbstractFloat

In addition to declaring StructTypes.NumberType(), custom types can also specify a specific, existing number type it should map to. It does this like:

StructTypes.numbertype(::Type{T}) = Float64

In this case, T declares it should map to an already-supported number type: Float64. This means that when deserializing, an input will be parsed/read/deserialiezd as a Float64 value, and then call T(x::Float64). Note that custom types may also overload StructTypes.construct(::Type{T}, x::Float64; kw...) if using a constructor isn't possible. Also note that the default for any type declared as StructTypes.NumberType() is Float64.

Similarly for serializing, Float64(x::T) will first be called before serializing the resulting Float64 value.

source

StructTypes.BoolType

StructTypes.BoolTypeType
StructTypes.StructType(::Type{T}) = StructTypes.BoolType()

Declare that T should map to a boolean value.

Types already declared as StructTypes.BoolType() include:

  • Bool

The interface to satisfy for deserializing is:

  • T(x::Bool): define a constructor that takes a single Bool value
  • StructTypes.construct(::Type{T}, x::Bool; kw...): alternatively, you may overload StructTypes.construct

The interface to satisfy for serializing is:

  • Bool(x::T): define a conversion to Bool method
source

StructTypes.NullType

StructTypes.NullTypeType
StructTypes.StructType(::Type{T}) = StructTypes.NullType()

Declare that T should map to a "null" value.

Types already declared as StructTypes.NullType() include:

  • nothing
  • missing

The interface to satisfy for serializing is:

  • T(): an empty constructor for T
  • StructTypes.construct(::Type{T}, x::Nothing; kw...): alternatively, you may overload StructTypes.construct

There is no interface for serializing; if a custom type is declared as StructTypes.NullType(), then serializing will be handled specially; writing null in JSON, NULL in SQL, etc.

source

CustomStruct

StructTypes.CustomStructType
StructTypes.StructType(::Type{T}) = StructTypes.CustomStruct()

Signal that T has a custom serialization/deserialization pattern that doesn't quite fit StructTypes.DataType or StructTypes.InterfaceType. One common example are wrapper types, where you want to serialize as the wrapped type and can reconstruct T manually from deserialized fields directly. Defining CustomStruct() requires overloading StructTypes.lower(x::T), which should return any serializable object, and optionally overload StructTypes.lowertype(::Type{T}), which returns the type of the lowered object (it returns Any by default). lowertype is used to deserialize an object, which is then passed to StructTypes.construct(T, obj) for construction (which defaults to calling T(obj)).

source
StructTypes.lowerFunction
StructTypes.lower(x::T)

"Unwrap" or otherwise transform x to another object that has a well-defined StructType definition. This is a required method for types declaring StructTypes.CustomStruct. Allows objects of type T to conveniently serialize/deserialize as another type, when their own structure/definition isn't significant. Useful for wrapper types. See also StructTypes.CustomStruct and StructType.lowertype.

source
StructTypes.lowertypeFunction
StructTypes.lowertype(::Type{T})

For StructTypes.CustomStruct types, they may optionally define lowertype to provide a "deserialization" type, which defaults to Any. When deserializing a type T, the deserializer will first call StructTypes.lowertype(T) = S and proceed with deserializing the type S that was returned. Once S has been deserialized, the deserializer will call StructTypes.construct(T, x::S). With the default of Any, deserializers should return an AbstractDict object where key/values can be enumerated/checked/retrieved to make it decently convenient for CustomStructs to construct themselves.

source

AbstractTypes

StructTypes.AbstractTypeType
StructTypes.StructType(::Type{T}) = StructTypes.AbstractType()

Signal that T is an abstract type, and when deserializing, one of its concrete subtypes will be materialized, based on a "type" key/field in the serialization object.

Thus, StructTypes.AbstractTypes must define StructTypes.subtypes, which should be a NamedTuple with subtype keys mapping to concrete Julia subtype values. You may optionally define StructTypes.subtypekey that indicates which input key/field name should be used for identifying the appropriate concrete subtype. A quick example using the JSON3.jl package should help illustrate proper use of this StructType:

abstract type Vehicle end

struct Car <: Vehicle
    type::String
    make::String
    model::String
    seatingCapacity::Int
    topSpeed::Float64
end

struct Truck <: Vehicle
    type::String
    make::String
    model::String
    payloadCapacity::Float64
end

StructTypes.StructType(::Type{Vehicle}) = StructTypes.AbstractType()
StructTypes.StructType(::Type{Car}) = StructTypes.Struct()
StructTypes.StructType(::Type{Truck}) = StructTypes.Struct()
StructTypes.subtypekey(::Type{Vehicle}) = :type
StructTypes.subtypes(::Type{Vehicle}) = (car=Car, truck=Truck)

# example from StructTypes deserialization
car = JSON3.read("""
{
    "type": "car",
    "make": "Mercedes-Benz",
    "model": "S500",
    "seatingCapacity": 5,
    "topSpeed": 250.1
}""", Vehicle)

Here we have a Vehicle type that is defined as a StructTypes.AbstractType(). We also have two concrete subtypes, Car and Truck. In addition to the StructType definition, we also define StructTypes.subtypekey(::Type{Vehicle}) = :type, which signals that when deserializing, when it encounters the type key, it should use the value, in the above example: car, to discover the appropriate concrete subtype to parse the structure as, in this case Car. The mapping of subtype key value to concrete Julia subtype is defined in our example via StructTypes.subtypes(::Type{Vehicle}) = (car=Car, truck=Truck). Thus, StructTypes.AbstractType is useful when the object to deserialize includes a "subtype" key-value pair that can be used to parse a specific, concrete type; in our example, parsing the structure as a Car instead of a Truck.

source

Utilities

Several utility functions are provided for fellow package authors wishing to utilize the StructTypes.StructType trait to integrate in their package. Due to the complexity of correctly handling the various configuration options with StructTypes.Mutable and some of the interface types, it's strongly recommended to rely on these utility functions and open issues for concerns or missing functionality.

StructTypes.constructfromFunction
StructTypes.constructfrom(T, obj)
StructTypes.constructfrom!(x::T, obj)

Construct an object of type T (StructTypes.constructfrom) or populate an existing object of type T (StructTypes.constructfrom!) from another object obj. Utilizes and respects StructTypes.jl package properties, querying the StructType of T and respecting various serialization/deserialization names, keyword args, etc.

Most typical use-case is construct a custom type T from an obj::AbstractDict, but constructfrom is fully generic, so the inverse is also supported (turning any custom struct into an AbstractDict). For example, an external service may be providing JSON data with an evolving schema; as opposed to trying a strict "typed parsing" like JSON3.read(json, T), it may be preferrable to setup a local custom struct with just the desired properties and call StructTypes.constructfrom(T, JSON3.read(json)). This would first do a generic parse of the JSON data into a JSON3.Object, which is an AbstractDict, which is then used as a "property source" to populate the fields of our custom type T.

source
StructTypes.constructFunction
StructTypes.construct(T, args...; kw...)

Function that custom types can overload for their T to construct an instance, given args... and kw.... The default definition is StructTypes.construct(T, args...; kw...) = T(args...; kw...).

source
StructTypes.construct(f, T) => T

Apply function f(i, name, FT) over each field index i, field name name, and field type FT of type T, passing the function results to T for construction, like T(x_1, x_2, ...). Note that any StructTypes.names mappings are applied, as well as field-specific keyword arguments via StructTypes.keywordargs.

source
StructTypes.foreachfieldFunction
StructTypes.foreachfield(f, x::T) => Nothing

Apply function f(i, name, FT, v; kw...) over each field index i, field name name, field type FT, field value v, and any kw keyword arguments defined in StructTypes.keywordargs for name in x. Nothing is returned and results from f are ignored. Similar to Base.foreach over collections.

Various "configurations" are respected when applying f to each field:

  • If keyword arguments have been defined for a field via StructTypes.keywordargs, they will be passed like f(i, name, FT, v; kw...)
  • If StructTypes.names has been defined, name will be the serialization name instead of the defined julia field name
  • If a field is undefined or empty and StructTypes.omitempties is defined, f won't be applied to that field
  • If a field has been excluded via StructTypes.excludes, it will be skipped
source
StructTypes.foreachfield(f, T) => Nothing

Apply function f(i, name, FT; kw...) over each field index i, field name name, field type FT, and any kw keyword arguments defined in StructTypes.keywordargs for name on type T. Nothing is returned and results from f are ignored. Similar to Base.foreach over collections.

Various "configurations" are respected when applying f to each field:

  • If keyword arguments have been defined for a field via StructTypes.keywordargs, they will be passed like f(i, name, FT, v; kw...)
  • If StructTypes.names has been defined, name will be the serialization name instead of the defined julia field name
  • If a field has been excluded via StructTypes.excludes, it will be skipped
source
StructTypes.mapfields!Function
StructTypes.mapfields!(f, x::T)

Applys the function f(i, name, FT; kw...) to each field index i, field name name, field type FT, and any kw defined in StructTypes.keywordargs for name of x, and calls setfield!(x, name, y) where y is returned from f.

This is a convenience function for working with StructTypes.Mutable, where a function can be applied over the fields of the mutable struct to set each field value. It respects the various StructTypes configurations in terms of skipping/naming/passing keyword arguments as defined.

source
StructTypes.applyfield!Function
StructTypes.applyfield!(f, x::T, nm::Symbol) => Bool

Convenience function for working with a StructTypes.Mutable object. For a given serialization name nm, apply the function f(i, name, FT; kw...) to the field index i, field name name, field type FT, and any keyword arguments kw defined in StructTypes.keywordargs, setting the field value to the return value of f. Various StructType configurations are respected like keyword arguments, names, and exclusions. applyfield! returns whether f was executed or not; if nm isn't a valid field name on x, false will be returned (important for applications where the input still needs to consume the field, like json parsing). Note that the input nm is treated as the serialization name, so any StructTypes.names mappings will be applied, and the function will be passed the Julia field name.

source
StructTypes.applyfieldFunction
StructTypes.applyfield(f, ::Type{T}, nm::Symbol) => Bool

Convenience function for working with a StructTypes.Mutable object. For a given serialization name nm, apply the function f(i, name, FT; kw...) to the field index i, field name name, field type FT, and any keyword arguments kw defined in StructTypes.keywordargs. Various StructType configurations are respected like keyword arguments, names, and exclusions. applyfield returns whether f was executed or not; if nm isn't a valid field name on x, false will be returned (important for applications where the input still needs to consume the field, like json parsing). Note that the input nm is treated as the serialization name, so any StructTypes.names mappings will be applied, and the function will be passed the Julia field name.

source

Macros

The StructType of a type can be set using the following utility macros

StructTypes.@StructMacro
@Struct(expr::Expr)
@Struct(expr::Symbol)

If expr is a struct definition, sets the StructType of the defined struct to Struct(). If expr is the name of a Type, sets the StructType of that type to Struct().

Examples

@Struct MyStruct

is equivalent to

StructTypes.StructType(::Type{MyStruct}) = StructType.Struct()

and

@Struct struct MyStruct
    val::Int
end

is equivalent to

struct MyStruct
    val::Int
end
StructTypes.StructType(::Type{MyStruct}) = StructType.Struct()
source

And similarly for

  • StructTypes.@Mutable
  • StructTypes.@CustomStruct
  • StructTypes.@OrderedStruct
  • StructTypes.@AbstractType
  • StructTypes.@DictType
  • StructTypes.@ArrayType
  • StructTypes.@StringType
  • StructTypes.@NumberType
  • StructTypes.@BoolType
  • StructTypes.@NullType