Honest nulls: what we return when a platform has no value

The Onekey team4 min read

A missing follower count is not zero, and an empty bio is not an empty string. How Onekey tells you a value is unknown, and how to read it without re-inventing the lie.

There is a quiet way for an API to be wrong: fill the gaps. A profile with no bio comes back with "bio": "". A post with no view count comes back with "views": 0. Both look like data. Neither is. The first says the user wrote nothing, the second says nobody looked, and in reality the source simply did not tell us.

Onekey has one rule for this, and it is short: null means unknown. This post is about what that rule covers, where it stops, and how to write code that respects it.

Value, null, or absent#

For a given endpoint, a field is in one of three states.

You seeIt means
A valueThe source provided it and it passed the type check for that field.
nullThe endpoint maps this field, but this record had no usable value.
The field is not thereThe endpoint does not map that field at all.

Here is a real response, recorded on 2026-09-30 from GET /v1/github/profile?username=torvalds:

json
{
  "author": {
    "id": "1024025",
    "username": "torvalds",
    "displayName": "Linus Torvalds",
    "followers": 325723,
    "following": 0,
    "url": "https://github.com/torvalds",
    "avatarUrl": "https://avatars.githubusercontent.com/u/1024025?v=4"
  },
  "engagement": { "comments": 12 },
  "content": { "text": null },
  "computed": { "language": null }
}

Three things are worth reading closely:

  • content.text is the bio. This user has none, so it is null. Not "".
  • author.following is 0, and that zero is real: the account follows nobody. A 0 and a null are different facts, and a mapper that flattened one into the other would make the second one unreadable.
  • There is no market or place group at all. This endpoint does not map them, so they are absent, not empty.

The rule for one endpoint is that every field it maps is present in every item, as a value or a null. So you can read data.author.followers without checking that the key exists. Across endpoints, check that the group is there first.

What makes a value null#

Two cases:

  1. The source left it empty. No bio, no view count, no license.
  2. The value failed the type check. Types are enforced by Onekey's mapper, not trusted from the source. A count that is not a number, or a link that is not an absolute http or https URL, becomes null rather than a string that would break your parser.

Here is another real example, from GET /v1/devto/article?id=4766672:

json
"engagement": { "likes": 116, "comments": 62, "views": null }

The likes and comments are numbers. The view count is null because that endpoint does not get one from the source for this article. If we had guessed, you would have a chart with a made-up column.

What Onekey does not do is equally important. It does not put 0 for a missing count, false for a missing flag or "" for missing text. It does not compute a value from another field to fill a gap. And a record the source says does not exist is a 404 NOT_FOUND, not an item full of nulls.

Reading nulls without undoing the work#

The API can be honest and your code can still throw that away. First, one real call to work with (1 credit):

import os, requests

body = requests.get("https://api.onekey.dev/v1/github/profile", params={"username": "torvalds"},
                    headers={"x-api-key": os.environ["ONEKEY_KEY"]}, timeout=30).json()

The commonest way to lose the information is a default:

followers = body["data"]["author"]["followers"] or 0     # turns unknown into zero

Both lines put the lie back. If a chart or a filter genuinely needs a number, decide that on purpose and keep the unknown visible somewhere. A version that keeps it:

def followers_or_none(body):
    if not body["success"]:
        raise RuntimeError(body["error"]["type"])
    return body["data"]["author"]["followers"]     # int or None

n = followers_or_none(body)
label = "unknown" if n is None else f"{n:,}"
print(label)

When you aggregate, drop the unknowns and say how many you dropped. An average over the 18 records that had a value is a fine number; an average over 20 records where two were quietly zero is not.

The same idea for derived values#

Some endpoints add a computed block: engagement_rate, estimated_reach and language. The rule carries over. In the profile above, computed.language is null because there is no text to detect a language from, and engagement_rate is null on a Hacker News item, which has neither a view count nor an author follower count to divide by. An explanation goes in data._warnings when there is something to say.

ext fields and nulls#

Platform-specific extras live under ext (for instance ext.karma on a Hacker News user). They follow the same rule, but their names are only stable for their own endpoint. If you need a value across platforms, use the fixed-schema field, and treat ext as a bonus. The data availability page has the full rules, including how stable and beta endpoints differ: on a beta endpoint, a field may be null where a value is expected, because the endpoint has not been verified against the live source yet.

Why this is worth the trouble#

A null costs you one branch in your code. A fabricated 0 costs you a wrong dashboard that nobody notices for a month. The honest version also makes the schema learnable: once you know that "mapped but empty" is null and "not mapped" is absent, every response, from any of the 266 platforms, reads the same way.

The terms are in the glossary as honest null and ext field, and the schema itself is on the unified schema page.