skip to content

What must an OData client's JSON parser change when a service moves from OData v2 verbose JSON to OData v4 JSON?

level: middleimportance: nice to knowfreq 9%

answer

  1. the envelope disappears
  2. double-underscore names become @ names
  3. numbers stop being strings
  4. dates lose /Date()/
  5. links you compute yourself

basics

~20 s

Nearly every structural assumption. OData v4 drops the "d"/"results" envelope and __metadata, reports counts and paging as @odata.count and @odata.nextLink, sends Int64 and Decimal as JSON numbers by default, writes dates as ISO 8601-style strings, and omits computable links.

solid answer

~40 s

In v2 verbose JSON every response sits inside `{"d": ...}`, a collection is `d.results`, each entry carries `__metadata` (`uri`, `type`, `etag`), unexpanded navigation properties are `__deferred` objects, and the total and next page are `__count` and `__next`. In v4 a collection is a top-level `value` array; identity, type and ETag become control information (`@odata.id`, `@odata.type`, `@odata.etag`), which minimal metadata omits wherever the client can compute it, navigation links included. The count is `@odata.count`, an Edm.Int64, and paging uses `@odata.nextLink`. Value encoding changed too: v2 sends Int64, Decimal and Double as strings and dates as `/Date(ms)/`, while v4 sends numbers as JSON numbers unless the client asks for `IEEE754Compatible=true`, and dates as ISO 8601 strings. 4.01 payloads may also drop the `odata.` prefix.

code

json · 13 lines
json
{
  "@odata.context": "https://api.example.com/svc/$metadata#Parts",
  "@odata.count": 1,
  "value": [
    {
      "@odata.etag": "W/\"4\"",
      "ID": 7,
      "Weight": 0.25,
      "Released": "2012-12-03T07:20:00Z"
    }
  ],
  "@odata.nextLink": "https://api.example.com/svc/Parts?$skiptoken=7"
}

go deeper

for a junior

Recall the envelope change: v2 nests data in d and d.results with __metadata; v4 returns a value array with @odata control information.

for a middle

Explain the encoding changes: numbers as JSON numbers unless IEEE754Compatible=true, ISO 8601 strings instead of /Date()/, and computable links omitted under minimal metadata.

for a senior

Show how you would harden a client for both 4.0 and 4.01 payloads by keying on OData-Version and testing precision on large Int64 and Decimal values.

for a principal

Weigh rewriting the parsing layer against a translating facade that serves v2-shaped JSON from a v4 service, given client count and remaining lifetime.

## Why a v2 parser fails on v4 OData v2's JSON format, usually called **verbose JSON**, and the OASIS v4 JSON format represent the same entity model, but almost nothing about the layout or the value encoding survived. A client that walks `d.results[i].__metadata.uri` finds nothing in a v4 body; one that parses every Int64 from a string throws on a JSON number. The changes fall into four groups. ## 1. The envelope - **v2**: every response is wrapped in an object with a single member `d`. The v2 JSON format adds it deliberately so the body is valid JSON but not an executable JavaScript statement. A collection sits in `d.results` (a 1.0 response puts the array directly in `d`). - **v4**: no wrapper. A collection of entities is an object with a **`value`** array; a single entity is the object itself. When the context control information is present it MUST be the first member. - **Single values**: a v2 request for one primitive property answers `{"d": {"results": {"Name": "Bread"}}}` at 2.0; v4 answers an object with a single `value` member, such as `{"@odata.context": "...", "value": "Bread"}`. The same `value` wrapper holds collections of primitive or complex values returned by a property or an operation. - **Expanded navigation**: both lines inline related entities requested with `$expand`, but in v4 the expanded entities carry v4 control information, not `__metadata`. ## 2. Per-entry metadata and links | Purpose | v2 verbose JSON | v4 JSON (4.0 names) | |---|---|---| | Canonical URL / identity | `__metadata.uri` | `@odata.id`, plus `@odata.editLink` / `@odata.readLink` | | Type name | `__metadata.type` | `@odata.type` | | Concurrency token | `__metadata.etag` | `@odata.etag` | | Unexpanded navigation | `{"__deferred": {"uri": ...}}` | `@odata.navigationLink`, often omitted | | Total count | `__count` | `@odata.count` | | Next page | `__next` | `@odata.nextLink` | The big behavioural change is the **odata.metadata** format parameter. With the default `minimal`, the service SHOULD remove control information the client can compute from `$metadata` and URL conventions. A navigation link equal to the read URL plus the navigation property's name may be left out, so a v4 entity usually shows **no trace** of its unexpanded navigation properties. A client that discovered relationships by scanning for `__deferred` must read them from `$metadata` instead, or request `full` metadata. ## 3. Value encoding - **Integers and decimals**: v2 writes `Edm.Int64`, `Edm.Decimal`, `Edm.Double`, `Edm.Single`, `Edm.Byte` and `Edm.Guid` as JSON strings (`"Price": "2.5"`). v4 writes all numeric types as **JSON numbers**, apart from the special values `INF`, `-INF` and `NaN`, which are strings. The format parameter **`IEEE754Compatible=true`** tells the service it MUST send `Edm.Int64` and `Edm.Decimal`, and the count, as strings, and a payload that uses strings MUST declare that parameter in its `Content-Type`. - **Count**: the v2 example shows `"__count": "3"` as a string; v4 `@odata.count` is an `Edm.Int64` number unless `IEEE754Compatible=true` applies. - **Dates**: v2 `Edm.DateTime` is `"/Date(694224000000)/"`, milliseconds since 1 January 1970. The v4 type list has no `Edm.DateTime`; `Edm.DateTimeOffset`, `Edm.Date`, `Edm.TimeOfDay` and `Edm.Duration` are JSON strings in the OData ABNF forms, for example `"2012-12-03T07:16:23Z"`. ## 4. What 4.01 changes again A 4.01 payload is still v4 JSON, but a parser must accept a few more forms: 1. Control information **SHOULD** drop the `odata.` prefix in 4.01 payloads (`@context`, `@count`, `@nextLink`). In 4.0 payloads it **MUST** be kept. 2. `@type` values for built-in primitive types no longer need the leading `#`. 3. Decimals may appear in exponential notation without any extra format parameter. 4. Deleted entities in a delta are marked with `@removed`. The rule that makes this manageable: decide from the response's `OData-Version` header, not by guessing from names. A conforming 4.01 consumer must also accept 4.0 payloads, including the prefixed names and the `#` on `@odata.type`. ## The same data, side by side ```json { "d": { "results": [ { "__metadata": { "uri": "https://api.example.com/svc/Parts(7)", "type": "Acme.Part", "etag": "W/\"4\"" }, "ID": 7, "Weight": "0.25", "Released": "/Date(1354519200000)/" } ], "__count": "1", "__next": "https://api.example.com/svc/Parts?$skiptoken=7" } } ``` The v4 equivalent is shown in the code example: one top-level object, a `value` array, `@odata.count` as a number, `@odata.nextLink`, `Weight` as a number and `Released` as an ISO 8601 string. ## A migration checklist for the parser - Read entries from `value`, not `d` or `d.results`. - Map `__metadata` uses to `@odata.id`, `@odata.type` and `@odata.etag`, and accept their absence. - Treat numbers as numbers, or send `IEEE754Compatible=true` if the runtime parses JSON numbers as IEEE 754 doubles. - Parse ISO 8601 date strings and stop expecting `/Date()/`. - Take relationships from `$metadata`, not from `__deferred`. - Key the prefix handling off `OData-Version`.

  • When should a client ask an OData v4 service for IEEE754Compatible=true?
    When its runtime parses every JSON number into an IEEE 754 double. Integers then lose precision past about 15 digits and decimals lose base-10 exactness. With the parameter, the service MUST send `Edm.Int64` and `Edm.Decimal` values, and the count, as strings. Without it, a service unable to produce an exact number MAY send a rounded one annotated with `Core.ValueException` carrying the exact value.
  • Must a 4.01 client still accept @odata.context as well as @context?
    Yes. The 4.01 JSON format requires a conforming consumer to accept OData 4.0 payloads, which MUST keep the `odata.` prefix and the `#` on primitive `@odata.type` values. 4.01 producers SHOULD omit both. The clean way to handle it is to read the response's `OData-Version` and parse by that version's rules.

saying these in an interview costs you the question

  • A v4 response still wraps data in d; only the inner names changed.
  • Int64 and Decimal values always arrive as JSON strings in OData JSON.
  • OData v4 dates still use the /Date(milliseconds)/ string from v2.
  • Every OData 4.01 payload must keep the odata. prefix on control information.
  • A v4 entity always lists its unexpanded navigation links, as __deferred did.