Skip to content

Comp Composite and clone lineage

Fredrik Dettner edited this page Oct 7, 2026 · 4 revisions

The Comp Composite and clone lineage

Fastighetsbenchmark models real estate as a small object graph. Understanding it unlocks the whole product. This section gives the mental model, a diagram, a fully worked end-to-end example, and the ingestion (create) flow.

The pieces

Everything in Fastighetsbenchmark is a Comp (a comparable object) with a natural key sedisId. There are a handful of business types, each exposed as its own resource (Endpoints reference):

  • Fastighet (/property-units) — a real property unit you own.
  • Samling (/samlingar) — your own collection. A container that exposes its children as containingCompSedisIds. You can create, replace, and delete these (the full lifecycle — see Ingestion (creating data)).
  • Jämförelseobjekt (/jamforelseobjekt) — a comparison zone you created by cloning a Sedis-owned SedisObjekt aggregate. (Swedish term retained — there is no clean English equivalent; think "comparison object".)
  • reference-zones (/reference-zones) — Sedis-curated external benchmark zones (Nils Holgersson, etc.). Shared reference data.

Note: The SedisObjekt source is not a fetchable resource. A SedisObjekt is a Sedis-owned, anonymized aggregate collection that you clone into a Jämförelseobjekt. You never own a SedisObjekt and never fetch one directly — you see only your clones of it. The relationship between your clone, the source aggregate's member property-units, and your own units is exposed through the bidirectional clone lineage below, not by reading the SedisObjekt itself.

Your Samling collections expose their children as containingCompSedisIds — an array of IDs, not nested objects. You follow up with ?sedisIdIn=... (comma-separated) to resolve them (this keeps payloads bounded — see Query conventions).

Clone lineage — the bidirectional opt-in pair

Clone membership is exposed as two opt-in (?fields=), tenant-filtered arrays of { sedisId, name } references — you only ever see your own rows in either direction, even when many customers clone the same shared SedisObjekt:

  • belongsToJamforelseobjekt (on a property-unit, or any Comp) — the reverse hop: the clones of mine whose source SedisObjekt contains this unit. Opt-in via ?fields=…,belongsToJamforelseobjekt. This is the field-form reverse of the ?belongsToJamforelseobjektSedisId= filter on /property-units.
  • containsPropertyUnits (on a Jämförelseobjekt only) — the forward hop: my property-units contained in this clone's source SedisObjekt. Opt-in via ?fields=…,containsPropertyUnits. Populated only on clone (Jämförelseobjekt) rows; absent on reference-zones.

The time-series values (the actual benchmarking numbers) live in CompDatum, fetched separately via /comp-data — they are never inlined into a Comp, because a single Comp can carry thousands of data points (see CompDatum below).

The graph

              SedisObjekt  (Sedis-owned aggregate; NOT a fetchable resource)
              sedisId = SO-STHLM
               │
   contains →  [ PU-1, PU-2, PU-3, ... ]   (members; tenant-filtered to YOURS)
               │                     │
   cloned by   │                     │ members are
   you         │                     ▼
               ▼                  PropertyUnit (Fastighet, yours)
       Jämförelseobjekt           sedisId = PU-1
       (your comparison zone)      │
       sedisId = JO-A              └── CompDatum time-series
                                          (via /comp-data)

  Forward lineage:  JO-A's   containsPropertyUnits:       [{ sedisId: "PU-1" }, ...]
  Reverse lineage:  PU-1's   belongsToJamforelseobjekt:   [{ sedisId: "JO-A" }]
                    (both opt-in, tenant-filtered — you see only YOUR rows)
graph TD
  SO["SedisObjekt SO-STHLM<br/>(Sedis aggregate; not fetchable)"]
  PU1["PropertyUnit PU-1 (yours)"]
  PU2["PropertyUnit PU-2 (yours)"]
  JO["Jämförelseobjekt JO-A (yours)<br/>clone of SO-STHLM"]
  CD["CompDatum time-series<br/>(via /comp-data)"]
  SO -->|contains members| PU1
  SO -->|contains members| PU2
  SO -.->|cloned by you| JO
  PU1 --> CD
  JO -.->|containsPropertyUnits<br/>forward, tenant-filtered| PU1
  PU1 -.->|belongsToJamforelseobjekt<br/>reverse, tenant-filtered| JO
Loading

Worked example (the canonical A6 flow)

Goal: "Show all my office property units within the Stockholm comparison zone, with Total return for the last 10 years, as a table + graph." Four calls:

1 — Find the comparison zone by name:

curl -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/jamforelseobjekt?nameContains=Stockholm"
# → data[0].sedisId = "JO-A"

2 — Find your office property units in that zone (reverse-lineage filter + property-type filter):

curl -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/property-units?belongsToJamforelseobjektSedisId=JO-A&propertyType=1"
# → data[*].sedisId = "PU-1", "PU-2", ...

Note: ?propertyType= takes the property-type id (an int), not a name. Each property unit's response carries propertyType: { id, name }; discover the id you want from the { id, name } pairs present in your data, then filter on that id. You can also filter by the type name with ?propertyTypeName= (a case-insensitive substring against the stored Swedish type name — see the caveat in Language and localization).

3 — Discover the parameter code for "Total return":

curl -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/parameters?nameContains=Total%20return"
# → data[0].code = "NX71"

4 — Pull the time-series (multi-value ?sedisIdIn=, date range):

curl -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/comp-data?sedisIdIn=PU-1,PU-2&parameterCode=NX71&fromDate=2016-01-01&toDate=2026-12-31"

You now have everything to render the table + graph.

CompDatum — the time-series

GET /fastighetsbenchmark/comp-data returns a flat, self-describing shape. Each row carries a dataType telling you which value member to read:

{
  "data": [
    { "sedisId": "PU-1", "parameterCode": "NX71", "valueDate": "2025-12-31", "dataType": "decimal", "figure": 7.42 },
    { "sedisId": "PU-1", "parameterCode": "BJ0",  "valueDate": "2025-12-31", "dataType": "boolean", "figure": 1, "boolean": true },
    { "sedisId": "PU-1", "parameterCode": "EB0",  "valueDate": "2025-12-31", "dataType": "enum",    "figure": 1, "enum": { "value": 1, "name": "Kontor", "enumType": "PropertyType" } },
    { "sedisId": "PU-1", "parameterCode": "DA0",  "valueDate": "2025-12-31", "dataType": "date",    "date": "2025-11-03" }
  ],
  "page": 1, "pageSize": 50, "totalCount": 4, "totalPages": 1
}

Note — the value is self-describing via dataType (read this carefully). A parameter's datatype is decided by its code's first letter (B = boolean, D = date, E = enum, anything else = a numeric decimal). Each CompDatum row sets dataType accordingly and populates exactly the matching member:

  • dataType: "decimal" → read figure (a decimal). The common case.
  • dataType: "boolean" → read boolean (true/false). figure also carries the raw 1/0 for transparency.
  • dataType: "enum" → read enum = { value, name, enumType }: value is the raw int, name the resolved label, enumType which domain it belongs to (e.g. PropertyType, CompClassification). figure also carries the raw int. Discover the full set of legal values for an enum parameter via GET /parameters/{code} → enumValues (see below).
  • dataType: "date" → read date (an ISO yyyy-MM-dd string — e.g. a transaction date). figure is absent.

Null members are omitted from the JSON, so only the member that matches dataType appears (plus the raw figure for boolean/enum). Note date is not valueDate: valueDate is the period the value applies to (part of the natural key); date is the date-typed value itself for D-prefixed parameters.

Note — a row is not necessarily a year. valueDate is the end of a quarter (31 Mar, 30 Jun, 30 Sep or 31 Dec), so a property unit can have several value dates per year once quarterly and half-yearly data is released; until then only 31 December is returned. Do not assume one row per year — group or filter on valueDate (?fromDate=/?toDate=). On a date other than 31 December, flows (rent, costs, net operating income) cover the 12 months ending on valueDate, and stocks (market value, area) are as of that date, so a 30 June value compares directly with a 31 December one. Sedis objects are calculated on 31 December only.

Discovering enum domains

For any enum (E-prefixed) parameter, GET /parameters/{code} lists the full legal-value domain in enumValues. This is how you map property-type ids to names, or list every valid classification:

curl -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/parameters/EB0"
# → { "code": "EB0", "name": "Property type", "dataType": "enum",
#     "enumValues": [ { "value": 1, "name": "Kontor" }, { "value": 2, "name": "Bostad" }, ... ] }

Non-enum parameters carry dataType (decimal/boolean/date) and omit enumValues.

Use ?count=false on /comp-data for fast bulk sync (Query conventions).

Ingestion (creating data)

The write endpoints let you push data into Fastighetsbenchmark. The POST endpoints require a mandatory Idempotency-Key header. A Samling additionally supports PUT (replace) and DELETE — the full lifecycle — and those two are idempotent by HTTP semantics, so they do not require the header.

Create a Samling

curl -X POST -H "X-Api-Key: $SEDIS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f9c1e2a-7b3d-4a1f-9c0e-2b6d4a8f1c33" \
  "https://api.sedis.se/v2/fastighetsbenchmark/samlingar" \
  -d '{ "name": "My Stockholm offices", "containingCompSedisIds": ["PU-1","PU-2"] }'
# → 201 Created, body is the created Samling (same shape as GET /samlingar/{id})
  • The owning customer is stamped from your API key — there is no owner field in the body, so you cannot create data for another tenant (Multi-tenant isolation).
  • Creating a Samling triggers Sedis's recalculation cascade so its derived values compute. The 201 returns synchronously; the recalc runs asynchronously in the background (Data freshness).
  • A name collision within your tenant returns 409 Conflict (Errors (RFC 7807)).

Update a Samling (replace name + members)

curl -X PUT -H "X-Api-Key: $SEDIS_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.sedis.se/v2/fastighetsbenchmark/samlingar/SAM-abc123" \
  -d '{ "name": "My Stockholm offices (2026)", "containingCompSedisIds": ["PU-1","PU-3"] }'
# → 200 OK, body is the updated Samling (same shape as GET /samlingar/{id})
  • Full replace, not a patch. The name and the containingCompSedisIds you send become the collection's entire state — members not listed are removed.
  • Because the member set changed, the update re-triggers the recalculation cascade so the collection's derived values recompute (Data freshness).
  • Tenant-scoped: a {sedisId} that is not your own Samling returns 404 Not Found (never 403 — Multi-tenant isolation).
  • The owner is taken from your API key, never the body — you cannot move a Samling to another tenant.
  • PUT is idempotent; it does not require an Idempotency-Key header.

Delete a Samling

curl -X DELETE -H "X-Api-Key: $SEDIS_API_KEY" \
  "https://api.sedis.se/v2/fastighetsbenchmark/samlingar/SAM-abc123"
# → 204 No Content
  • Deletes the Samling and cascades its data/tags cleanup. The member property-units themselves are not deleted — only the collection and its membership.
  • Tenant-scoped: deleting a {sedisId} that is not your own returns 404 Not Found.
  • DELETE is idempotent; a second delete of the same id returns 404. No Idempotency-Key header is required.

Uploading values and creating property units

Property units are created with POST property-units and updated with PUT — an upload of values (POST comp-uploads) never creates one. The full guide, with the typed values, the strict 422 contract and locations, is Writing data.

Idempotency-Key semantics

  • Mandatory on every v2 POST. Missing/blank (or longer than 255 characters) → 400 with invalidFields: ["Idempotency-Key"].
  • Use a fresh UUID per logical operation.
  • Replaying the same key (e.g. after a network timeout) returns the original response without re-executing — no duplicate Samling, no double ingest. Replays carry an Idempotency-Replayed: true header.
  • The replay window is 24 hours, scoped to your tenant + key.
  • A retry with the same key while the first request is still running → 409; retry after it finishes to get the replay.
  • The same key with a different request (another method, path or body) → 422 with type https://api.sedis.se/problems/idempotency-key-reused — never the other request's response. Whitespace and member order in the JSON body do not count as different.
  • Keys are kept by Sedis across restarts and server instances (since 2.3.0), so a retry is safe even if it lands on another server.
  • If the first request fails with a server error, nothing is kept — the retry runs again.

Clone this wiki locally