References
The single most important concept to understand about this client is how it
handles references. ESPN’s API is hypermedia-driven: almost every resource
links to related resources through a $ref URL rather than embedding them.
This client embraces that model rather than hiding it.
The $ref model
A typical ESPN resource embeds links like this:
{
"id": "2578570",
"fullName": "Jacoby Brissett",
"position": {
"$ref": "http://sports.core.api.espn.com/v2/.../positions/8?lang=en"
},
"team": {
"$ref": "http://sports.core.api.espn.com/v2/.../teams/22?lang=en"
}
}
Even though ESPN nests a position object here, the only meaningful content
is the $ref link. The client therefore does not map the nested object.
Instead it captures the link as a string property.
The naming convention
The client relies on a strict property naming convention to capture links:
A single
$refsub-object namedfoois stored in a string propertyfooReference.An array of
$refsub-objects namedfoois stored in an array propertyfooReferences.
So the JSON above populates:
$athlete->getPositionReference();
// "http://sports.core.api.espn.com/v2/.../positions/8?lang=en"
$athlete->getTeamReference();
// "http://sports.core.api.espn.com/v2/.../teams/22?lang=en"
And an athlete’s list of injury links populates:
$athlete->getInjuriesReferences();
// [
// "http://sports.core.api.espn.com/v2/.../injuries/123?lang=en",
// "http://sports.core.api.espn.com/v2/.../injuries/456?lang=en",
// ]
Note
This is why DTOs never contain nested “child” objects for linked
resources. A property called positionReference is a deliberate signal
that the value is a link you can resolve, not embedded data. Embedded
value objects (an address, a status) that are genuinely part of the
parent resource are still mapped as nested DTOs — only $ref links
become reference strings.
The reference normalizer
Capturing {name}Reference and {name}References from arbitrary $ref
sub-objects is not something the stock Symfony serializer does on its own. A
custom normalizer performs this mapping during deserialization: it inspects
each sub-object, detects a $ref key, and writes the link into the matching
reference property.
When you wire the client (see Installation), register this normalizer
alongside the standard ones so reference properties are populated. Without it,
scalar fields will still deserialize correctly, but reference properties will
remain null.
Note
The reference normalizer is the one piece of serializer configuration the
client genuinely depends on. If you find that getXxxReference() always
returns null even though the raw JSON clearly contains a $ref, a
missing or unregistered normalizer is almost always the cause.
Resolving a reference
A reference is just a fully-qualified ESPN URL, so you can resolve it directly
through the client’s low-level get() method by turning the string into a
UriInterface and naming the DTO class you expect:
use HansPeterOrding\EspnApiClient\Dto\EspnPosition;
$ref = $athlete->getPositionReference();
$position = $client->get(
$client->getUriFactory()->createUri($ref),
EspnPosition::class
);
echo $position->getDisplayName(); // "Quarterback"
The same approach resolves every reference, whether it came from a DTO
property or from a listRefs* call:
use HansPeterOrding\EspnApiClient\Dto\EspnAthlete;
$athleteRefs = $client->seasons()->athletes()->listRefsForTeam(2025, 12);
foreach ($athleteRefs as $ref) {
$athlete = $client->get(
$client->getUriFactory()->createUri($ref),
EspnAthlete::class
);
echo $athlete->getFullName() . PHP_EOL;
}
Why references instead of eager loading?
Resolving every link eagerly would turn a single athlete fetch into dozens of cascading HTTP requests (position, team, college, contracts, statistics, projections, event log …). By keeping links as strings, the client lets you decide exactly which related resources are worth a round trip. This keeps the client fast, predictable, and free of hidden network traffic.
Read next
Error handling — how failed requests surface as exceptions
Extending — adding endpoints or customizing serialization