NAME
API::Docker::Role::Type - Instance behaviour of a generated API::Docker::Type class
VERSION
version 0.004
SYNOPSIS
# composed automatically by `use API::Docker::Type;`
my $hc = API::Docker::Type::HostConfig->from_data($from_the_daemon);
my $wire = $hc->TO_JSON; # CamelCase keys, JSON booleans
my $bytes = $hc->to_json;
DESCRIPTION
Every class under API::Docker::Type::* composes this role; it is applied by API::Docker::Type's import, so a generated class never names it.
The role reads the attribute registry API::Docker::Type writes -- it never walks the object's own keys. A field that is an attribute but not in the registry is invisible here, which is exactly what maint/spec-drift-check.pl exists to catch.
The two entry points have different jobs
"from_data" inflates an engine response: its keys are the daemon's, so only the registry's wire names are read and everything else is preserved under the name it arrived with. "new" assembles a request out of what a caller wrote: its keys are the caller's, so the Perl spelling is read first and the wire spelling is an alias for it.
The split is what keeps the passthrough invariant true. Resolving a Perl name on the response path renames the engine's data -- Docker's swagger spells 114 fields with a lowercase first letter, so a lowercase key off an engine is ordinary rather than exotic, and reading id as the Perl name of Id both loses the field it really was and rewrites one we did know (karr k85).
The same split decides what happens to a value that does not fit its declared type. "from_data" keeps it -- unset attribute, raw value in "unknown_fields", name in "rejected_fields" -- because one divergent field must not make the rest of a usable response unreachable. "new" croaks, because there the value is the caller's and a mistake worth stopping on.
A nested hashref follows whichever entry point started the construction, so one object graph is read one way throughout.
unknown_fields
A HashRef of everything that reached this object under a name the model could not translate, kept under the name it arrived with and handed back out by "TO_JSON" unchanged. Two things land here: a name the registry does not know at all, and -- on the response path only -- a known wire name whose value did not fit the type the swagger declares for it. "rejected_fields" is what tells the two apart.
This is the whole reason a caller whose engine is newer than the swagger this model was generated from still gets their field to the daemon. Translating what we know and forwarding the rest verbatim is worth more to this distribution than a tidy model: a field the caller set must never be dropped because we have not heard of it.
That promise covers the value as well as the name, undef included: a name the model does not know has no declared type, so there is no zero value we could read a null as, and inventing one would be us deciding what the engine meant. A known field's null is the opposite case and is read as unset -- see "A null on a known field is read as unset".
rejected_fields
A HashRef naming the fields this object could not use, mapping the wire name the value arrived under to the Perl attribute it would have filled.
It exists so that "the engine did not send this" and "the engine sent it and the model could not use it" are two different observations. Both leave the typed accessor undef; only the second puts the field's wire name in here, and the value itself in "unknown_fields" beside it:
my $c = API::Docker::Type::ContainerInspectResponse->from_data({
Id => 'x', State => 'exited' }); # the swagger says State is an object
$c->state # undef
$c->rejected_fields->{State} # 'state' -- sent, and refused
$c->unknown_fields->{State} # 'exited' -- kept as it arrived
$c->TO_JSON->{State} # 'exited' -- and written back out
Only "from_data" fills it. "new" is strict and croaks instead, so an object a caller built has an empty one.
new
my $hc = API::Docker::Type::HostConfig->new(privileged => 1);
my $hc = API::Docker::Type::HostConfig->new(Privileged => 1); # the same
Builds an object from data a caller wrote, which is what a request payload is. Keys are matched against the Perl attribute names first and the registry's wire names second, so either spelling reaches the attribute; anything else is kept in "unknown_fields", exactly as on the response path.
Two keys that resolve to one attribute -- privileged and Privileged together -- are refused. Which one won was decided by hash order and nothing else, measured at nine zeroes and eleven ones over twenty constructions of the same arguments. They are refused even where the two values agree: values that happen to match are not the same thing as an unambiguous call, and the caller should be shown the mistake rather than the luck.
Use "from_data" for a decoded engine response. That is the other half of the same split: there the keys are the daemon's, not the caller's, and only the wire spelling may be read.
from_data
my $mount = API::Docker::Type::Mount->from_data($hashref);
my $c = $class->from_data($hashref, client => $docker);
Builds an object from a decoded daemon response. Keys are matched against the registry's wire names; nested objects, arrays of objects and hashes of objects are inflated the same way. Anything else is kept in "unknown_fields" under the name it arrived with.
Pairs after the hashref are attributes that did not come from the engine -- the client a composed entity role declares in _entity_attributes is the one this distribution has. They are kept apart from $hashref on purpose: what the daemon sent and what we are adding are two different things, and a daemon that one day sends a key of that name should not be able to overwrite ours.
A decoded response is a map of wire names and nothing else, so that is the only name space this reads. The Perl spelling is not a second chance here, and deliberately so: Docker's swagger gives 114 fields a wire name whose first letter is lowercase -- BuildInfo.id is one -- so a lowercase key off an engine is ordinary. Reading such a key as the Perl name of a field we do know would rename the engine's data and lose the field it really was. Use "new" where both spellings should be accepted; that is where a caller, not an engine, is the author of the keys.
A value that does not fit costs one field, not the response
Where a value disagrees with the type the swagger declares -- a State that is the bare status string rather than the object ContainerInspectResponse declares -- the field is not set, the response still inflates, and the raw value is kept in "unknown_fields" under its wire name with the name recorded in "rejected_fields":
$c->state # undef
$c->rejected_fields->{State} # 'state'
$c->unknown_fields->{State} # 'exited'
$c->TO_JSON->{State} # 'exited', byte for byte
We are not the authority on what an engine answers. Podman announces API 1.44 and Docker 1.55 on the machine this was written on, while the model is generated from v1.51, and one divergent field making every other field of an otherwise usable inspect unreachable is not an improvement. The typed accessor keeps its contract either way: if it is set, it is the declared type.
This leniency is the response path's alone. "new" croaks on a value that does not fit, because there the value came from the caller and is a mistake worth stopping on rather than an engine being itself.
A null on a known field is read as unset
An engine answering "Tags": null is saying what an engine that omits the field is saying, and this reads both the same way: the attribute stays undef, nothing is filed in "unknown_fields" or "rejected_fields", and "TO_JSON" writes no key for it. The null is not carried and the key does not come back.
That is not a convenience, it is the daemon's own resolution. Measured 2026-08-28 against Podman 5.8.4 (API 1.44) on POST /containers/create, with an image name nothing can resolve so that the body is parsed and no container is created:
{} 500 parsing reference "": repository name must ...
{"Image":null} 500 parsing reference "": repository name must ...
{"Image":""} 500 parsing reference "": repository name must ...
Byte-identical, all three. Go's encoding/json unmarshals a null into the type's zero value, and an absent field leaves that same zero value behind -- "" for a string, nil for a map or a slice, false for a bool -- so the daemon cannot tell an explicit null from an absent field, and collapsing the two loses no meaning it could have expressed. It holds outbound too, which is why a null reaches us where a key could simply have been left out: a nil slice marshals to null, and GET /images/{id}/history answers "Tags": null for a layer that carries no tag (karr k93).
Three things that all look like a null therefore behave differently, on purpose:
my $n = API::Docker::Type::Network->from_data({
Options => { 'com.docker.x' => undef }, # a key the caller chose
IPAM => { Options => undef, # a field we know
FutureNested => undef }, # a field we do not
});
$n->TO_JSON # { Options => { 'com.docker.x' => undef },
# IPAM => { FutureNested => undef } }
The known field's null is a statement the daemon could have made in two ways, so its key goes. The unknown field's null is data we cannot type, so it stays. A null under a key the caller chose -- the additionalProperties shape, see "Keys that are the caller's data" in API::Docker::Type -- is that caller's value under that caller's key, and stays as well.
t/type_fixture_passthrough.t holds all three against the captured fixtures, at every depth rather than at the top level only.
from_json
my $mount = API::Docker::Type::Mount->from_json($bytes);
"from_data" on a JSON document. The argument is a UTF-8 encoded byte string, exactly what "to_json" produces.
TO_JSON
my $struct = $host_config->TO_JSON;
The structure the daemon expects: registry wire names as keys, JSON booleans for Bool, nested objects serialised by their own TO_JSON.
An attribute that was never set is omitted, not sent as null -- Docker tells an absent flag apart from a false one, and so does this. A known field the engine sent as an explicit null is such an attribute, so its key does not come back either; that is measured rather than assumed, see "A null on a known field is read as unset". The contents of "unknown_fields" are written first and a set field wins over them -- and a null kept there, under a name the model could not translate, does go out as a null.
That precedence never costs a value "from_data" preserved. A field lands in unknown_fields under a known wire name only when its typed attribute was left unset -- that is what being rejected means -- and an unset attribute is one this loop skips, so the two do not meet. They meet only where someone sets the attribute afterwards, or hands new a unknown_fields entry beside the field of that name; in both of those the typed value is the later and more deliberate one, and it is the one that goes out.
to_json
my $bytes = $host_config->to_json;
"TO_JSON" encoded as a UTF-8 byte string, canonical so two equal objects encode to the same bytes.
docker_attributes
my $info = API::Docker::Type::HostConfig->docker_attributes;
The class's merged attribute registry as a HashRef keyed by Perl attribute name. Each entry carries wire, type (a type descriptor, see API::Docker::Type), since, required and enum, plus the isa and coerce the Moo attribute was declared with, which is what "from_data" asks before handing a value to the constructor. maint/spec-drift-check.pl reads the first five.
docker_attribute_order
my $names = API::Docker::Type::HostConfig->docker_attribute_order;
The Perl attribute names in declaration order, inherited ones first -- which is the order the fields appear in the swagger.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-api-docker/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.