DEV Community

William Baptist
William Baptist

Posted on

Distinguishing Missing and Null JSON Fields in Python

An export has a field in one record, null in another and no field at all in a third. Those are different cases. This read-only checker reports missing names separately from explicit null values, without treating zero or an empty string as missing.

Python 3.10 or later and a plain-text editor are all you need. The program uses only the standard library.

1. Create the practice exports

JSON is a text format for named values and lists. null is an explicit JSON value; it is not the same as leaving a name out. Save this as MakeExamples.py in a practice folder:

from pathlib import Path


Path("Records.json").write_bytes(
    b'[{"Id":"001","Note":null},{"Id":"002"},{"Id":"003","Note":""}]\n'
)
Path("Values.json").write_bytes(
    b'[{"Value":0},{"Value":false},{"Value":""},{"Value":[]},{"Value":{}}]\n'
)
Enter fullscreen mode Exit fullscreen mode

Run:

python3 MakeExamples.py
Enter fullscreen mode Exit fullscreen mode

Use your installation's Python 3 command if it is not named python3. This creates or replaces two files, so use a folder without files you need to keep. Records.json gives three different states for Note. Values.json contains values that a simple true/false test might accidentally treat as absent.

2. Save the checker

Save this as CheckRequiredFields.py beside the examples:

import json as Json
import sys as Sys
from pathlib import Path


def RejectConstant(Token):
    raise ValueError("non-JSON numeric token")


def UniqueObject(Pairs):
    Result = {}
    for Name, Value in Pairs:
        if Name in Result:
            raise ValueError("repeated object name")
        Result[Name] = Value
    return Result


def CheckRequiredFields(FileName, Names):
    if not Names or any(not Name.strip() for Name in Names):
        raise ValueError("choose nonblank field names")
    if len(Names) != len(set(Names)):
        raise ValueError("choose each field once")
    with Path(FileName).open("rb") as Input:
        Data = Input.read(1048577)
    if len(Data) > 1048576:
        raise ValueError("input exceeds 1 MiB teaching limit")
    Records = Json.loads(Data.decode("utf-8"), parse_constant=RejectConstant,
                         parse_float=str, object_pairs_hook=UniqueObject)
    if not isinstance(Records, list) or any(not isinstance(Item, dict) for Item in Records):
        raise ValueError("expected an array of objects")
    if len(Records) > 1000:
        raise ValueError("more than 1000 records")
    Missing = []
    Null = []
    for Number, Record in enumerate(Records, start=1):
        for Name in Names:
            if Name not in Record:
                Missing.append({"Record": Number, "Field": Name})
            elif Record[Name] is None:
                Null.append({"Record": Number, "Field": Name})
    return {"Records": len(Records), "MissingFields": Missing, "NullFields": Null}


def Main():
    if len(Sys.argv) < 3:
        print("Usage: python3 CheckRequiredFields.py input.json FIELD [FIELD ...]", file=Sys.stderr)
        return 2
    try:
        Report = CheckRequiredFields(Sys.argv[1], Sys.argv[2:])
    except (OSError, ValueError, RecursionError) as Problem:
        print(f"Check stopped: {Problem}", file=Sys.stderr)
        return 2
    print(Json.dumps(Report, indent=2, ensure_ascii=True))
    return 1 if Report["MissingFields"] or Report["NullFields"] else 0


if __name__ == "__main__":
    Sys.exit(Main())
Enter fullscreen mode Exit fullscreen mode

You choose the field names on the command line. Each name must be nonblank and chosen once. Matching is exact: Note and note differ. Only direct fields in each record are checked, not names inside nested objects. A field name containing a space needs quoting in your command window.

The input must be a UTF-8 JSON array of objects, without an initial byte-order mark. The file limit is 1 MiB (1,048,576 bytes), with one extra byte read to detect oversized input, and the array limit is 1,000 records. Repeated object names and non-JSON numeric tokens such as NaN ("not a number") are rejected throughout the parsed file. Deep nesting can stop with a recursion error; the byte cap is not a complete resource-safety guarantee.

parse_float=str keeps decimal/exponent tokens from being converted to binary floating-point values during this check. The program does not save those temporary parsed values or change the file. It only uses membership and an explicit null comparison for the report.

3. Test membership before the value

Name not in Record tests whether the name exists in the dictionary, Python's collection of named values. It does not ask whether the value is true or false.

When the name is present, Record[Name] is None tests for Python's representation of JSON null. The elif keeps a missing name out of the null list and avoids trying to read a value that is not there.

The lists preserve record order and your chosen field order. Record numbers start at one. Field names appear in the report, but the values do not.

4. Check missing and null separately

Run:

python3 CheckRequiredFields.py Records.json Id Note
Enter fullscreen mode Exit fullscreen mode

The output is:

{
  "Records": 3,
  "MissingFields": [
    {
      "Record": 2,
      "Field": "Note"
    }
  ],
  "NullFields": [
    {
      "Record": 1,
      "Field": "Note"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Record 1 has Note with value null. Record 2 lacks Note. Record 3 has Note with an empty string, so neither rule flags it.

The exit code is 1 when either category is present, 0 when neither is found, and 2 when usage, reading, parsing, the input shape or a teaching limit stops the check. A stopped run prints no JSON report. Treating null as a review item is this checker's rule, not a claim that every receiving system forbids it.

5. Keep zero and empty values distinct

Run:

python3 CheckRequiredFields.py Values.json Value
Enter fullscreen mode Exit fullscreen mode

The output is:

{
  "Records": 5,
  "MissingFields": [],
  "NullFields": []
}
Enter fullscreen mode Exit fullscreen mode

The exit code is 0. Zero, false, an empty string, an empty array and an empty object are all present and are not null. Whether they are useful or allowed is a separate question.

Now change only the requested name:

python3 CheckRequiredFields.py Values.json value
Enter fullscreen mode Exit fullscreen mode

The lowercase value is absent from all five records, so MissingFields lists records 1 through 5, NullFields stays empty and the exit code is 1. The checker reports the exact name you asked for. It cannot tell a misspelling from a genuinely required field; compare your command with the documented field names before interpreting the report.

A schema is a set of rules for expected fields and values. This is not a complete schema check. It does not validate types, formats, ranges, identifiers, nested fields or a particular importer's behaviour. An empty array also passes with zero records. Choose fields from the receiving system's documented requirements, and review its rules for null and empty values separately.

It never fills missing fields or replaces null. Those changes can invent meaning, so keep the original export and check the source before correcting anything.

Use a saved regular file that will not change while it is read. Save reports under a new filename: redirecting output over an input can empty it before Python opens it. Names, counts and positions can reveal information, so keep reports private when they describe work or personal data.

References

Top comments (0)