Is That Temperature a Number or Just Text?
Extract structured payloads with explicit JSON validity and type checks while preserving the distinction between missing, null, text and numeric data.
14 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Structured Messages Entered Relational Queries
BOTIE
That convenience does not erase the payload’s types. A numeric property, a text property and an absent property still mean different things. Our report uses that distinction before it allows a value into a temperature calculation.
Sources for this history
The Payload Looks Familiar, but Its Types Have Changed
One sensor sends temperature as the number 42. Another sends the text “42”. A third sends no temperature at all, and an interrupted upload is not valid JSON. Botie must not treat these records as interchangeable just because a person can guess what some of them meant.
JSON lets a row carry a structured payload, but extraction still needs a contract. We will validate the text, inspect the property’s JSON type, then accept only the numeric values this report promises to use.
The records are invented maintenance examples. The workshop exposes record operations; the SQL worksheet executes a real query in an isolated SQLite database.
Step 1 of 1
Similar-looking payloads take different routes
The report accepts only a JSON integer or real at $.temperature.
| Payload | Property type | Accepted value |
|---|---|---|
| {"temperature":42} | integer | 42 |
| {"temperature":"42"} | text | Rejected |
| {} | Missing | Rejected |
| {"temperature":null} | null | Rejected |
| broken | Invalid JSON | Rejected |
| {"temperature":true} | true | Rejected |
{"temperature":42}
- Property type
- integer
- Accepted value
- 42
{"temperature":"42"}
- Property type
- text
- Accepted value
- Rejected
{}
- Property type
- Missing
- Accepted value
- Rejected
{"temperature":null}
- Property type
- null
- Accepted value
- Rejected
broken
- Property type
- Invalid JSON
- Accepted value
- Rejected
{"temperature":true}
- Property type
- true
- Accepted value
- Rejected
The type gate belongs before numeric analysis
Type-aware extraction precedes filtering, so zero is preserved and a boolean is not mistaken for a measurement.
Inspect the payload before accepting its temperature
Read only valid numeric temperature properties, filter the unknown extraction results, and preserve record identity.
BOTIE
Record IDs with accepted temperatures
[
{
"record_id": 1,
"temperature": 42
}
]Follow the operations
1. JSON payload records
[ { "record_id": 1, "payload": "{\"temperature\":42}" }, { "record_id": 2, "payload": "{\"temperature\":\"42\"}" }, { "record_id": 3, "payload": "{}" }, { "record_id": 4, "payload": "broken" }, { "record_id": 5, "payload": "{\"temperature\":true}" } ]2. Read a numeric JSON property
[ { "record_id": 1, "payload": "{\"temperature\":42}", "temperature": 42 }, { "record_id": 2, "payload": "{\"temperature\":\"42\"}", "temperature": null }, { "record_id": 3, "payload": "{}", "temperature": null }, { "record_id": 4, "payload": "broken", "temperature": null }, { "record_id": 5, "payload": "{\"temperature\":true}", "temperature": null } ]3. Check for a missing value
[ { "record_id": 1, "payload": "{\"temperature\":42}", "temperature": 42 } ]4. Choose table columns
[ { "record_id": 1, "temperature": 42 } ]5. Order rows by named fields
[ { "record_id": 1, "temperature": 42 } ]6. Result
[ { "record_id": 1, "temperature": 42 } ]
The workshop uses finite JavaScript numbers and top-level JSON properties. Units, physical ranges and duplicate-key policy require separate ingestion validation.
1. Separate valid JSON from the right schema
json_valid asks whether the text follows the supported JSON syntax. A valid array, object or scalar can pass that check while still being the wrong structure for a sensor record.
Our contract expects a top-level temperature property whose JSON type is integer or real. It does not parse units from strings or convert booleans into readings. Production ingestion should also impose size limits, a schema and a policy for duplicate property names.
Syntax validation is the first boundary. It is not proof that the message has a useful meaning.
2. Read a path and inspect the JSON type
The path $.temperature addresses the temperature property at the root. json_type reports a type label such as integer, real, text or null; an absent path returns SQL NULL. json_extract returns a corresponding SQL scalar for a single scalar path.
A JSON null property and a missing property can both extract as SQL NULL, so retain the type result when that distinction matters. A boolean can extract numerically, which is why extraction alone is not the temperature check.
Check the JSON type before trusting the extracted SQL value.
Distinguish null from absence
These expressions use valid payloads so the missing-value distinction is isolated.
Database setup for this example
SELECT json_type('{"temperature":null}', '$.temperature'),
json_type('{}', '$.temperature'),
json_extract('{"temperature":true}', '$.temperature');| json_type('{"temperature":null}', '$.temperature') | json_type('{}', '$.temperature') | json_extract('{"temperature":true}', '$.temperature') |
|---|---|---|
| null | NULL | 1 |
The string label null identifies an explicit JSON null. The final 1 represents a JSON boolean, not a valid temperature under our contract.
3. Guard extraction with a lazy CASE branch
Calling JSON functions on malformed text can raise an error. The nested CASE first checks json_valid, then checks the property type, then extracts the accepted value. SQLite evaluates only the required CASE branch.
Do not assume arbitrary AND conditions will always execute left to right to protect a failing expression. Put the protection into an expression with the required evaluation semantics.
The invalid payload follows the unknown extraction route without crashing the whole report.
Inspect the extraction for every source record
This query retains rejected records as NULL extractions so the type boundary is visible.
| record_id | payload |
|---|---|
| 1 | {"temperature":42} |
| 2 | {"temperature":"42"} |
| 3 | {} |
| 4 | {"temperature":null} |
| 5 | broken |
| 6 | {"temperature":true} |
Database setup for this example
CREATE TABLE telemetry(record_id INTEGER PRIMARY KEY, payload TEXT);
INSERT INTO telemetry VALUES (1,'{"temperature":42}'),(2,'{"temperature":"42"}'),(3,'{}'),(4,'{"temperature":null}'),(5,'broken'),(6,'{"temperature":true}');WITH extracted AS (
SELECT record_id,
CASE WHEN json_valid(payload) THEN
CASE WHEN json_type(payload, '$.temperature') IN ('integer','real')
THEN json_extract(payload, '$.temperature') END
END AS temperature
FROM telemetry
)
SELECT record_id, temperature FROM extracted
ORDER BY record_id;| record_id | temperature |
|---|---|
| 1 | 42 |
| 2 | NULL |
| 3 | NULL |
| 4 | NULL |
| 5 | NULL |
| 6 | NULL |
The text 42 and boolean true follow the rejected route even though a loose conversion could produce numbers. The challenge selects only accepted rows.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Should this record pass?
Use the information below.
The payload is {"temperature":true}. json_extract returns 1 in SQLite. The report accepts JSON integer or real only.4. Choose what to do with rejected payloads
This teaching report omits invalid or unsuitable temperatures. A real pipeline should count and investigate those omissions, preserve a safe reference to the original event and avoid filling them with invented zero readings.
Range and unit checks would follow the type check. A valid numeric value is not necessarily a plausible Celsius measurement, and the same number can mean a different physical quantity under a different unit.
The worksheet tests type extraction. It does not claim to validate a complete sensor contract.
5. Expand arrays without forgetting the new row grain
json_each returns one row for each top-level array element or object member. When joined to a source row containing an array, it can multiply that row. Carry the original record ID and the element key or position if you need to identify each result.
Do not sum an original invoice total after expanding its line-item array unless the intended aggregation accounts for that repetition. The same fan-out issue from relational joins appears here.
An array expansion changes the output grain from payload to payload element.
Inspect one sensor’s tagged components
For an array, json_each supplies zero-based positions in key.
Database setup for this example
SELECT key, value FROM json_each('["bearing","seal"]') ORDER BY key;| key | value |
|---|---|
| 0 | bearing |
| 1 | seal |
Those two rows are two elements from one message, not two independent messages.
6. Promote stable fields when they become part of the core contract
Keeping an original JSON payload can be useful for occasional details. Frequently filtered or joined properties often deserve typed columns, constraints and an indexing strategy that fits the database. Otherwise every query repeats parsing and policy decisions.
Avoid assuming JSON syntax is portable across engines. The functions here use SQLite JSON text support available in the lesson runtime, rather than newer JSONB features or another database’s operators.
The representation should make the contract easier to enforce and the common queries easier to inspect.
7. Test zero, negatives and lookalike types
Zero and negative numbers are still numbers. Whether they are physically valid is a later domain rule. The extraction should therefore retain zero and negative numeric values while excluding the strings that resemble them.
The workshop makes the typed property extraction a visible component, followed by a NULL filter. Inspect that intermediate result to see which records were rejected before changing the downstream analysis.
A truthiness check would lose a legitimate zero reading. Use the declared type and missing-value policy.
Check yourself
Questions on Parts 1 to 7
Question 1 of 2
What should be returned?
Use the information below.
The payload is {"temperature":0}. The extraction contract accepts numeric values and has no range restriction.Try it with Botie
Build with Botie
Write and run SQL