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.Struct
— TypeStructTypes.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")
StructTypes.Mutable
StructTypes.Mutable
— TypeStructTypes.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 theserializedfield1
key, thejuliafield1
field ofT
will be set. When serializing thejuliafield2
field ofT
, the output key will beserializedfield2
. Field name mappings are provided as aTuple
ofTuple{Symbol, Symbol}
s, i.e. each field mapping is a Julia field nameSymbol
(first) and serialized field nameSymbol
(second).StructTypes.excludes(::Type{T}) = (:field1, :field2)
: specify fields ofT
to ignore when serializing and deserializing, provided as aTuple
ofSymbol
s. When deserializing, iffield1
is encountered as an input key, it's value will be read, but the field will not be set inT
. When serializing,field1
will be skipped when serializing outT
fields as key-value pairs.StructTypes.omitempties(::Type{T}) = (:field1, :field2)
: specify fields ofT
that shouldn't be serialized if they are "empty", provided as aTuple
ofSymbol
s. This only affects serializing. If a field is a collection (AbstractDict, AbstractArray, etc.) andisempty(x) === true
, then it will not be serialized. If a field is#undef
, it will not be serialized. If a field isnothing
, it will not be serialized. To apply this to all fields ofT
, setStructTypes.omitempties(::Type{T}) = true
. You can customize this behavior. For example, by default,missing
is not considered to be "empty". If you wantmissing
to be considered "empty" when serializing your typeMyType
, 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 typeT
that should be passed to functions that set values for this field. DefineStructTypes.keywordargs
as a NamedTuple of NamedTuples.
Support functions for StructTypes.DataType
s:
StructTypes.names
— FunctionStructTypes.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
.
StructTypes.excludes
— FunctionStructTypes.excludes(::Type{T}) = (:field1, :field2)
Specify for a StructTypes.Mutable
StructType
the fields, given as a Tuple
of Symbol
s, that should be ignored when deserializing, and excluded from serializing.
StructTypes.omitempties
— FunctionStructTypes.omitempties(::Type{T}) = (:field1, :field2)
StructTypes.omitempties(::Type{T}) = true
Specify for a StructTypes.Mutable
StructType
the fields, given as a Tuple
of Symbol
s, 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
StructTypes.keywordargs
— FunctionStructTypes.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 NamedTuple
s, 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.
StructTypes.idproperty
— FunctionStructTypes.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.
StructTypes.fieldprefix
— FunctionStructTypes.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
.
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.DictType
— TypeStructTypes.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 aDict{Symbol, Any}
of input key-value pairsStructTypes.construct(::Type{T}, x::Dict; kw...)
: alternatively, you may overload theStructTypes.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 thepairs
iteration function (from Base) to iterate key-value pairs to be serializedStructTypes.keyvaluepairs(x::T)
: alternatively, you can overload theStructTypes.keyvaluepairs
function if overloadingpairs
isn't possible for whatever reason
StructTypes.ArrayType
StructTypes.ArrayType
— TypeStructTypes.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 aVector
argument of values and constructs aT
StructTypes.construct(::Type{T}, x::Vector; kw...)
: alternatively, you may overload theStructTypes.construct
method for your type if defining a constructor isn't possible- Optional:
Base.IteratorEltype(::Type{T}) = Base.HasEltype()
andBase.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 subtypeAbstractArray
and definegetindex(x::T, i::Int)
, then iteration is inherited for your type
StructTypes.StringType
StructTypes.StringType
— TypeStructTypes.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 argumentStructTypes.construct(::Type{T}, x::String; kw...)
: alternatively, you may overloadStructTypes.construct
for your typeStructTypes.construct(::Type{T}, ptr::Ptr{UInt8}, len::Int; kw...)
: another option is to overloadStructTypes.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 bothStructTypes.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)
: overloadBase.string
for your type to return a "stringified" value, or more specifically, that returns anAbstractString
, and should implementncodeunits(x)
andcodeunit(x, i)
.
StructTypes.NumberType
StructTypes.NumberType
— TypeStructTypes.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.
StructTypes.BoolType
StructTypes.BoolType
— TypeStructTypes.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 singleBool
valueStructTypes.construct(::Type{T}, x::Bool; kw...)
: alternatively, you may overloadStructTypes.construct
The interface to satisfy for serializing is:
Bool(x::T)
: define a conversion toBool
method
StructTypes.NullType
StructTypes.NullType
— TypeStructTypes.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 forT
StructTypes.construct(::Type{T}, x::Nothing; kw...)
: alternatively, you may overloadStructTypes.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.
CustomStruct
StructTypes.CustomStruct
— TypeStructTypes.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)
).
StructTypes.lower
— FunctionStructTypes.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
.
StructTypes.lowertype
— FunctionStructTypes.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 CustomStruct
s to construct themselves.
AbstractTypes
StructTypes.AbstractType
— TypeStructTypes.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.AbstractType
s 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
.
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.constructfrom
— FunctionStructTypes.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
.
StructTypes.construct
— FunctionStructTypes.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...)
.
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
.
StructTypes.foreachfield
— FunctionStructTypes.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 likef(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
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 likef(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
StructTypes.mapfields!
— FunctionStructTypes.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.
StructTypes.applyfield!
— FunctionStructTypes.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.
StructTypes.applyfield
— FunctionStructTypes.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.