[AUGUST 19, 2026]
"false" is truthy: the astrology API bug hiding in your retrograde check
Several astrology APIs return booleans and numbers as strings. Here's the exact bug that causes, why it's silent, and what a correctly typed response looks like.
Here is a line of JavaScript that looks correct and is wrong:
if (planet.is_retro) {
showRetrogradeBadge(planet);
}
If is_retro came from an API that returns "false" — the string — this branch runs
for every planet, every time. "false" is a non-empty string, and non-empty strings are
truthy. Your UI shows a retrograde badge on the Sun. Nobody catches it in review because
the code reads like English.
This isn't hypothetical. We captured a real response from a leading astrology API for Mars on a sample chart:
{
"name": "Mars",
"full_degree": "235.9835",
"speed": "0.9614368",
"is_retro": "false",
"is_combusted": "false",
"sign_no": 8,
"house": "9"
}
Every one of those quoted values is a type bug waiting for a caller who forgets to coerce. And there are a lot of places to forget.
The four ways string-typed data breaks
1. Truthiness. The bug above. "false", "0", "no" are all truthy. The only safe
check is === "true", and every developer on the team has to remember that, every time.
2. Arithmetic. "235.9835" + 30 is "235.983530". String concatenation, not
addition. You wanted the longitude thirty degrees ahead; you got a longer string. Sorting
is worse: ["9", "10", "2"].sort() gives ["10", "2", "9"]. House numbers as strings sort
wrong.
3. Precision loss you can't undo. "235.9835" is four decimal places. Swiss Ephemeris
computes the underlying value to roughly fifteen. Once the API has formatted it to a
four-decimal string, the rest is gone — you cannot recover it client-side. For most UI
this doesn't matter. For matching engines, research, or anything that composes several
calculations, rounding early accumulates.
4. Every consumer re-parses. parseFloat, === "true", Number(...) sprinkled
through the codebase. Each one is a place a new engineer can get wrong, and none of them
should exist.
What a typed response looks like
Same planet, same birth data, from AstroAsk:
{
"planet": "Mars",
"longitude": 235.98352051331312,
"speed": 0.7117720663103958,
"isRetrograde": false,
"isCombust": false,
"sign": 7,
"house": 9
}
- Numbers are JSON numbers, at full double precision. Round on your side if you want; we never throw information away.
- Booleans are JSON booleans.
if (planet.isRetrograde)does what it says. - Integers are integers.
signandhousesort and compare correctly.
That's it. There's no clever technique here — it's just returning the types the values actually have. The reason it's worth a blog post is that it's surprisingly uncommon in this category, and the bugs it prevents are the silent kind.
If you're stuck with a string-typed API
You can't fix the upstream, but you can quarantine it. Write one normalizer at the API boundary and never touch raw fields elsewhere:
function normalizePlanet(raw) {
return {
planet: raw.name,
longitude: Number(raw.full_degree),
speed: Number(raw.speed),
isRetrograde: raw.is_retro === 'true',
isCombust: raw.is_combusted === 'true',
house: Number(raw.house),
};
}
Every downstream consumer gets real types; the coercion lives in exactly one place. If you later switch providers, that's also the only function you have to change — which is roughly what our DivineAPI migration guide walks through, field by field.
Try it
Run POST /v1/planets in the playground and look at the types in the
response. Or grab a free trial key — 5,000 requests, no card — and diff it
against whatever you're using today. The numbers will match. The types won't.