Reverse engineering UDisc's API - Part 4 - SmartLayouts - Hole Details, Tees, and Targets
Published June 9, 2025
3 min read
Tags: APIs, typescript
In previous parts of this series, we explored how UDisc structures its course and
detail data using schema maps and densely packed JSON arrays. Now we’re going a
level deeper — into the smartLayouts
array and the rich, interconnected
structure that defines each hole
in a layout.
What is a SmartLayout
In UDisc, a smartLayout
is a curated list of hole configurations —
representing a specific way to play the course, like a tournament layout, long
tees, or winter alt positions.
Each course will have a smartLayouts
field that is an array of ID. Each ID
points to a layout configuration with specific hole details.
smartLayouts: [345, 3534, 12323, ...]
Resolve SmartLayout
objects
We can resolve the layout entries using our resolveByIds
utility, passing in
the smart layout schema map and data array.
const layouts = resolveByIds(layoutMap, data);
Each layout will includes fields like name
, holes
, and various other layout
details.
{
_id: '2daCeAPCLnsEQKntM',
layoutId: 106061,
courseId: 1523,
type: 'smart',
name: 'Glitch World Championship Qualifer',
details: 'Layout for the Glitch World Championship Qualifier on 6/1/2025!',
status: 'active',
holes: [
22549, 22566, 22583,
22599, 22619, 22641,
22658, 22674, 22691,
22707, 23074, 23088,
23867, 23886, 24225,
24244, 24675, 24692
],
sortIndex: 1998146,
areLayoutSelectionsValid: true,
playCount30: 15,
lengthBin: 'intermediate',
typicalHoleLengthLowerMeters: 66.925,
typicalHoleLengthUpperMeters: 124.263,
floorsAscended: 17,
floorsDescended: 21,
stepCount: 4797,
time: 130.71908333333334,
parRoundRating: 182.73106384277344,
activationDate: undefined,
deactivationDate: undefined,
level: undefined,
difficultyBin: 'intermediate',
technicalityBin: 'technical',
holeDistance: { _398: 24717, _400: 24718 }
}
Resolve Holes
Now it's time to figure out more about each hole.
First, we have to follow the holes
field which is an array of IDs.
Similar to above, we use resolveByIds
:
const holesSchema = resolveByIds(holes, data);
That will gives us an array of objects that turns out to be, you guessed it, a schema map.
[
{
_352: 353,
_354: 4133,
_72: 356,
_357: 19527,
_374: 19532,
_385: 19538,
_387: 388,
_389: 4146,
_147: 256,
_366: 7,
_1627: 131,
_1628: 7,
_707: 19539,
_367: -5,
_368: 19721,
_393: 19722,
_159: 19723,
_396: 19724
},
...
]
We can now take this, iterate over it, and resolve each object of IDs:
holesSchema.forEach((schema) => {
holesDecoded.push(resolveKeyAndValueNames(schema, data));
});
And we finally get to see some hole data:
[
{
holeId: 'KRFy',
pathConfigurationId: 'qXAC',
name: '1',
status: 'active',
teePosition: {
_359: 360,
_294: 22551,
_147: 256,
_149: 362,
_152: 363,
_186: 22552,
_366: 7,
_367: 21,
_368: 22554
},
targetPosition: {
_376: 377,
_158: 22556,
_147: 256,
_149: 380,
_152: 381,
_223: 22558,
_366: 7
},
doglegs: [],
par: 3,
distance: 80.749,
description: '',
isTemporary: false,
notes: 'OB: Pond (defined by white stakes). Play, with a one stroke penalty, from last place disc was in bounds or re-tee.',
teeSign: { _370: 371, _372: 373 },
teePad: { _149: 362, _152: 363 },
basket: { _149: 380, _152: 381 },
holeDistance: { _398: 399, _400: 390 }
},
{
holeId: 'L43O',
pathConfigurationId: 'zwYI',
name: '2',
status: 'active',
teePosition: {
_359: 406,
_294: 22568,
_147: 256,
_149: 409,
_152: 410,
_186: 22569,
_366: 7
},
targetPosition: {
_376: 415,
_158: 22573,
_147: 256,
_149: 418,
_152: 419,
_223: 22575,
_366: 7
},
doglegs: [],
par: 3,
distance: 70.07600000000001,
notes: undefined,
teeSign: undefined,
teePad: { _149: 409, _152: 410 },
basket: { _149: 418, _152: 419 },
holeDistance: { _398: 429, _400: 425 }
},
...
]
Introducing deepHydrate
export function deepHydrate<T>(input: T, data: unknown[]): T {
if (Array.isArray(input)) {
return input.map(item => deepHydrate(item, data)) as T;
}
if (typeof input !== 'object' || input === null) return input;
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input)) {
if (isSchemaMap(value)) {
result[key] = resolveKeyAndValueNames(value, data);
} else if (Array.isArray(value)) {
result[key] = value.map(v =>
isSchemaMap(v) ? resolveKeyAndValueNames(v, data) : deepHydrate(v, data)
);
} else {
result[key] = deepHydrate(value, data);
}
}
return result as T;
}
The deepHydrate
function recursively walks through any input object (or array)
and resolves all embedded schema maps into human-readable objects using
resolveKeyAndValueNames
.
One caveat: deepHydrate
only resolves schema maps found in the original
structure. If a resolved field itself contains a new schema map, that won't be
resolved unless you hydrate it separately or extend the function recursively.
Calling this function on each hole will show you what I mean:
{
holeId: 'KRFy',
pathConfigurationId: 'qXAC',
name: '1',
status: 'active',
teePosition: {
teePositionId: 'XMhc',
teeType: { _292: 293, _147: 256, _294: 295 },
status: 'active',
latitude: 42.2765401,
longitude: -71.896142,
teePositionLabels: [ 22553 ],
isTemporary: false,
notes: '',
teeSign: { _370: 371, _372: 373 }
},
targetPosition: {
targetPositionId: 'PQ1G',
targetType: {
_254: 255,
_147: 256,
_104: 159,
_72: 193,
_257: 22557,
_194: 195
},
status: 'active',
latitude: 42.2770589,
longitude: -71.8968277,
targetPositionLabels: [ 22559, 22560 ],
isTemporary: false
},
doglegs: {},
par: 3,
distance: 80.749,
description: '',
isTemporary: false,
notes: 'OB: Pond (defined by white stakes). Play, with a one stroke penalty, from last place disc was in bounds or re-tee.',
teeSign: {
imageUrl: 'https://udisc-parse.s3.amazonaws.com/league/c9f63559-3322-46c5-aedb-9bd8211e03c0_Red1.png',
optimizedUrl: 'https://udisc-parse.s3.amazonaws.com/r_c9f63559-3322-46c5-aedb-9bd8211e03c0_Red1.png'
},
teePad: { latitude: 42.2765401, longitude: -71.896142 },
basket: { latitude: 42.2770589, longitude: -71.8968277 },
holeDistance: { feet: 264.92454915999997, meters: 80.749 }
}
You can see the fields like teePosition
and targetPosition
have been resolved
but since they also contained schema maps, there are still unresolved fields.
I am working on updating deepHydrate
to walk new schema maps it finds
but that will be for another post.
We are getting into some really cool data now. You could come up with some neat map visualizations using the longitude and latitude. Maybe you want to find the average distance for holes on the Disc Golf Pro Tour and figure out how many holes Forrest Gump would have played. The possibilities are endless.
Summary
In this post, we dug into how UDisc represents hole layouts using the smartLayouts structure. We walk through resolving layout metadata, decoding individual holes with schema maps, and using deepHydrate to unpack nested fields. It gets us most of the way there — tees, targets, distances, etc. — but we also run into a limit: deepHydrate only resolves the first layer. If the resolved fields themselves contain schema maps, those stay untouched (for now).
This post highlights how deeply structured and graph-like the data really is — and sets up where we're headed next with full hydration and maybe even some event/live scoring stuff.