fieldmesh

Validated against FROST-Server#

Run 2026-10-07 against fraunhoferiosb/frost-server:latest with postgis/postgis:16-3.4. Companion to sdk/sensorthings.py.

The completeness matrix in VIRTUAL_FARM.md had this row as a claim: a serializer exists, but "existing tools work on day one" is untested until a real implementation has accepted its documents. FROST-Server is the right oracle — it is the most widely deployed STA server, from the same institute as the 2.0 editor, so what it accepts is what the ecosystem accepts in practice.

Method#

Stand FROST up, deep-insert a Thing carrying a Datastream, ObservedProperty, Sensor, Observation and FeatureOfInterest shaped exactly as the adapter emits them, read them back, and diff key-by-key against the adapter's own output for the same logical entity. FROST assigns its own ids and selfLinks, so those are excluded from the diff; everything else is compared.

What held#

The deep insert returned 201 Created. The document shapes are valid 1.1, not merely plausible.

resultQuality survived as a list of arbitrary objects, nested evidence included. §3.3's flag set therefore maps natively onto 1.1's DQ_Element with multiplicity zero-to-many, exactly as sensorthings-2.0.md predicted — and it is the capability the 2.0 draft drops, which is the concrete argument for raising Q18.

parameters/fieldmesh survived intact. The namespacing decision — layer-zero fields nested rather than posing as 1.1's "environmental conditions during measurement" — costs nothing in the reference implementation.

resultTime: null on an Observation was accepted and returned as null, so the honest encoding of "layer zero does not record when the result was generated" is legal rather than a workaround.

phenomenonTime as an instant with Z, unitOfMeasurement as {name, symbol, definition} with a UCUM definition, and a Datastream properties object carrying kind, derived_from, expected_interval_s, thresholds and health were all accepted and returned verbatim.

After the two fixes below, the Datastream and Observation documents are at full key parity with FROST's.

What the oracle caught#

resultTime on a Datastream. 1.1 Table 10 makes it zero-to-one. The adapter emitted it as null; FROST omits it. A bare null for an optional field layer zero has nothing to say about is noise, so the adapter now omits it. Observation's resultTime is "One (mandatory)" and nullable, so that one stays null — the two fields look alike and are not.

A defensible divergence, now a parameter#

@iot.count on an entity set. FROST omits it unless $count=true is asked for. 1.1's own Example 11 shows it in the response to a plain entity-set request with no query options. Both read the spec reasonably, so this is not a bug in either: collection(..., count=...) makes it switchable, defaults to including it per Example 11, and an HTTP layer passes $count through.

A finding about ids, not a defect#

FROST with default configuration rejects a client-supplied string id:

400 Cannot deserialize value of type java.lang.Long from String "a-uuid-like-string"

Its ids are Long. §1 requires UUIDv7, and 1.1 does not constrain the id type, so the adapter is within the standard; FROST is configurable in this respect (persistence_idGenerationMode and friends). The practical consequence is worth stating: FROST cannot be used to round-trip our ids without reconfiguration, so it serves as an oracle for document shape, not as a store for layer-zero data. Nothing in the spec needs to change.

Since done#

What was outstanding here — a GIS client reading these documents, which needed the HTTP layer with $filter, $expand, $top and paging — has been done: QGIS 3.44 and Fraunhofer's client read the service and found five defects no internal test could reach (qgis-validation.md).

Three more questions put to the oracle, 2026-10-07#

Asked while validating the HTTP layer in server/ against two real clients — see qgis-validation.md for the full account.

Question FROST's answer What it settled
Does $top=2 suppress @iot.nextLink? No. Two rows plus ?$top=2&$skip=2. With $count=true, the count is the unfiltered total and the nextLink carries $count forward Our paging matches the reference implementation exactly. A change to suppress the nextLink on an explicit $top was drafted on a reading of OData and not made, because the oracle outranks the reading
Is unitOfMeasurement required when observationType is OM_CountObservation? Yes. With a unit: 201. Without: 400 Our count-with-a-unit document was a semantic defect, not a protocol violation — and a raw datastream declaring a null unit could not be mirrored into FROST, which constrains Q21
Can a Thing be deep-inserted with Datastreams and Observations and no Location? No. No FeatureOfInterest provided, and none can be generated. FROST generates features of interest from Thing Locations; layer zero derives them from the datastream's declared feature (§3.1). A different and deliberate choice, but anyone mirroring into FROST will meet it

Reproducing#

docker network create frostnet
docker run -d --name frost-db --network frostnet \
  -e POSTGRES_DB=sensorthings -e POSTGRES_USER=sensorthings \
  -e POSTGRES_PASSWORD=ChangeMe postgis/postgis:16-3.4
docker run -d --name frost --network frostnet -p 8080:8080 \
  -e persistence_db_driver=org.postgresql.Driver \
  -e persistence_db_url=jdbc:postgresql://frost-db:5432/sensorthings \
  -e persistence_db_username=sensorthings -e persistence_db_password=ChangeMe \
  -e persistence_autoUpdateDatabase=true \
  -e serviceRootUrl=http://localhost:8080/FROST-Server \
  fraunhoferiosb/frost-server:latest
curl -s http://localhost:8080/FROST-Server/v1.1/

The findings are pinned as tests in sdk/test_sensorthings.py (TestValidatedAgainstFrost), so they survive without the container.