Handed an unfamiliar OData service, how can you tell from its responses whether it speaks OData v2 or OData v4?
answer
- two lines, two header names
- the outer JSON wrapper
- where the collection array sits
- edmx root: namespace and Version
basics
~20 sRead the version header and the JSON envelope. OData v2 sends DataServiceVersion and wraps JSON in a "d" object with __metadata on each entry; OData v4 sends OData-Version (4.0 or 4.01) and puts a collection in a top-level "value" array.
solid answer
~40 sThe fastest signal is the response header: the Microsoft-published v2 line uses `DataServiceVersion` (for example `DataServiceVersion: 2.0`), while a v4 service MUST send `OData-Version: 4.0` or `4.01`. The JSON body confirms it: v2's verbose JSON wraps every response in `{"d": ...}`, puts a collection under `results`, and gives each entry a `__metadata` object with `uri` and `type`; v4 JSON is a plain object whose collection is the `value` array, with control information such as `@odata.context` (`@context` in 4.01 payloads). The metadata document settles any doubt: a v4 `edmx:Edmx` root lives in the `http://docs.oasis-open.org/odata/ns/edmx` namespace and MUST carry `Version="4.0"` or `"4.01"`, while v2 metadata uses Microsoft's `schemas.microsoft.com/ado/...` namespaces. It matters because a v2 client cannot read v4 URLs or payloads unchanged.
go deeper
Recall the three tells: DataServiceVersion versus OData-Version, the "d" wrapper versus a "value" array, and the OASIS edmx namespace with its Version attribute.
Explain which tells are mandatory: OData-Version MUST appear on v4 responses, while v2's header is only encouraged, so payload shape and $metadata are the reliable checks.
Show you check the protocol line before choosing a client generator or parser, and that you know a v2 server may answer in 1.0 shape when nothing newer is needed.
Frame version identification as the first step of any OData integration estimate, since a v2 estate changes the URL, payload and tooling assumptions of the whole plan.
## Two lines that share one name **OData** (the Open Data Protocol) is a set of conventions for exposing a queryable data model over HTTP. It exists in two lineages that are **not wire-compatible**: | Line | Published by | Status | Version headers | |---|---|---|---| | v1 and v2 (v3 followed them) | Microsoft, as open specifications on odata.org | superseded, still running in many large enterprise estates | `DataServiceVersion`, `MaxDataServiceVersion` | | 4.0 | OASIS, OASIS Standard of 24 February 2014 (errata through 2016) | widely deployed | `OData-Version`, `OData-MaxVersion` | | 4.01 | OASIS, OASIS Standard of 23 April 2020 | the current standard | same as 4.0 | Because URLs such as `Products(1)` and options such as `$filter` look alike in both, people assume one client can talk to either. It cannot: the payload envelope, several URL forms and the version handshake all changed. So the first job with an unknown service is to find out which line it speaks. ## Signal 1: the version header - **v2**: the overview specification defines `DataServiceVersion` on requests and responses and `MaxDataServiceVersion` on requests. Sending them is only *highly encouraged*, and a response `DataServiceVersion` *should* be present, so a v2 response without one is still legal. - **v4**: the protocol says services **MUST** include `OData-Version` on every response, with the value `4.0` or `4.01`. A response with `OData-Version` is v4; one with `DataServiceVersion` is the older line. A missing header is therefore a weak hint of the older line, never proof. ## Signal 2: the JSON envelope The two lines shape the same data very differently. A v2 collection response: ```json { "d": { "results": [ { "__metadata": { "uri": "https://api.example.com/svc/Parts(7)", "type": "Acme.Part" }, "ID": 7, "Name": "Hex bolt", "Supplier": { "__deferred": { "uri": "https://api.example.com/svc/Parts(7)/Supplier" } } } ], "__count": "1" } } ``` The same data from a v4 service with the default minimal metadata: ```json { "@odata.context": "https://api.example.com/svc/$metadata#Parts", "@odata.count": 1, "value": [ { "ID": 7, "Name": "Hex bolt" } ] } ``` The markers to look for: - **`d`** as the single outer member: v2 (and v1). The v2 JSON format adds it on purpose, so a response is valid JSON but not an executable JavaScript statement. - **`results`**, **`__metadata`**, **`__deferred`**, **`__count`**, **`__next`**: v2 verbose JSON. - **`value`** plus names starting with **`@`**: v4. In a 4.0 payload they carry the `odata.` prefix (`@odata.context`); a 4.01 payload SHOULD drop it (`@context`). One trap: a v2 server answers with the *lowest* version that can serve the request, and a 1.0 response puts the entries straight into `"d": [ ... ]` without `results`. Seeing `d` is enough to place it on the old line. ## Signal 3: the metadata document Both lines publish `$metadata`, but the documents differ: 1. A v4 document's root is `edmx:Edmx` in the namespace `http://docs.oasis-open.org/odata/ns/edmx`, and it **MUST** contain a `Version` attribute of `4.0` or `4.01`. 2. Its model elements use `http://docs.oasis-open.org/odata/ns/edm`. 3. Older documents use Microsoft namespaces, for example `http://schemas.microsoft.com/ado/2008/09/edm` for CSDL 2.0, and v2 metadata carries a `DataServiceVersion` attribute so a consumer can check whether it understands the constructs. ## Signals that do not discriminate - Keys in parentheses (`Parts(7)`) exist in both lines. - `$filter`, `$orderby`, `$top`, `$skip`, `$expand`, `$select` and `$format` exist in both. - The `/$count` path segment returns a bare number in both. - Atom XML existed in both lines, though 4.01 never updated the Atom format. ## A probe sequence 1. `GET` the service root and one entity set with `Accept: application/json`, and read the response headers: `OData-Version` settles it as v4, and its value tells 4.0 from 4.01. 2. Look at the body: `d` means the old line, `value` with `@`-names means v4. 3. `GET $metadata` and read the root element's namespace and `Version`. A v4 service SHOULD also advertise its supported versions through the `Core.ODataVersions` annotation on its entity container, a space-separated list in which 4.01 implies 4.0. ## Why it matters Choosing the wrong client generation, parser or query syntax produces errors that look like data bugs: an empty list because the code read `value` from a `d` envelope, or a 400 because a v4 filter function was sent to a v2 service. Check the header, the envelope and `$metadata` before writing a line of integration code, and record the answer in the integration's documentation so the next team does not repeat the probe.
- Why does an OData v2 JSON response wrap everything in a "d" object?The v2 JSON format says it is a security measure: wrapping every response in an object whose single member is `d` makes the payload valid JSON but not a valid JavaScript statement, so a page cannot execute it as a script in a cross-site attack. Request payloads are not wrapped. OData v4 JSON has no `d` member: a collection response is an object whose entries sit in `value`.
- A response carries neither DataServiceVersion nor OData-Version. What can you conclude?Little from the header alone. v2 only says servers should send `DataServiceVersion`, so its absence is legal on the old line, while a v4 service MUST send `OData-Version`, so a v4 service omitting it is non-conformant. Fall back on the payload envelope (`d` versus `value`) and on the namespace and `Version` attribute of `$metadata`.
saying these in an interview costs you the question
- OData v4 is v2 with a few extra query options, so the same client works.
- A $metadata endpoint or keys like Parts(7) prove a service is OData v4.
- The "d" wrapper is how OData v4 marks a data payload.
- OData 4.01 renamed its version header when it dropped the OData- prefix.
- A v2 response without DataServiceVersion is malformed and must be rejected.