← BACK TO BLOG

[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. sign and house sort 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.