Skip to content

    Serving OData APIs

    Feature Overview

    OData is an OASIS standard, which essentially enhances plain REST with standardized query options like $select, $expand, $filter, etc. Find a rough overview of the feature coverage in the following table.

    Query Options Remarks Node.js Java
    $value Retrieves single rows/values
    $count Gets number of rows for paged results
    $top,$skip Requests paginated results
    $select Like SQL select clause
    $orderby Like SQL order by clause
    $filter Like SQL where clause
    $expand Deep-read associated entities (1) (2)
    $search Search in multiple/all text elements(3)
    $apply For data aggregation
    Lambda Operators Boolean expressions on a collection (4)
    Delta Payload For nested entity collections in deep update in prog.
    • (1) Support for nested $select, $expand, $filter and $orderby options.
    • (2) Support for nested $select, $expand, $filter, $orderby, $top and $skip options.
    • (3) The elements to be searched are specified with the annotation.
    • (4) Current limitation: Navigation path identifying the collection can only contain one segment.

    Learn more in the Getting Started guide on Learn more in the tutorials Take a Deep Dive into OData.

    Mapping of CDS Types

    The table below lists CDS’s built-in types and their mapping to the OData EDM type system.

    CDS Type OData V4
    UUID Edm.Guid (1).
    Boolean Edm.Boolean
    UInt8 Edm.Byte
    Int16 Edm.Int16
    Int32 Edm.Int32
    Integer Edm.Int32
    Int64 Edm.Int64
    Integer64 Edm.Int64
    Decimal Edm.Decimal
    Double Edm.Double
    Date Edm.Date
    Time Edm.TimeOfDay
    DateTime Edm.DateTimeOffset
    Timestamp Edm.DateTimeOffset with Precision=”7”
    String Edm.String
    Binary Edm.Binary
    LargeBinary Edm.Binary
    LargeString Edm.String

    (1) Mapping can be changed with, for example, @odata.Type='Edm.String'

    OData V2 has the following differences:

    CDS Type OData V2
    Date Edm.DateTime with sap:display-format="Date"
    Time Edm.Time

    Overriding Type Mapping

    Override standard type mappings using the annotation @odata.Type first, and then additionally define @odata {MaxLength, Precision, Scale, SRID}.

    @odata.Type is effective on scalar CDS types only and the value must be a valid OData (EDM) primitive type for the specified protocol version. Unknown types and non-matching facets are silently ignored. No further value constraint checks are applied.

    They allow, for example, to produce additional OData EDM types which are not available in the standard type mapping. This is done during the import of external service APIs, see Using Services.

    entity Foo {
      @odata: { Type: 'Edm.GeometryPolygon', SRID: 0 }
      geoCollection : LargeBinary;

    Another prominent use case is the CDS type UUID, which maps to Edm.Guid by default. However, the OData standard puts up restrictive rules for Edm.Guid values - for example, only hyphenated strings are allowed - which can conflict with existing data. Therefore, you can overridde the default mapping as follows:

    entity Books {
      key ID : UUID @odata.Type:'Edm.String';

    ❗ Warning
    It is possible to “cast” any scalar CDS type into any (in-)compatible EDM type:

    entity Foo {
      // ...
      @odata: {Type: 'Edm.Decimal', Scale: 'floating' }
      str: String(17) default '17.4';

    This translates into the following OData API contract:

    <Property Name="str" Type="Edm.Decimal" Scale="floating" DefaultValue="17.4"/>

    The client can now rightfully expect that float numbers are transmitted but in reality the values are still strings. There is no automatic data conversion behind the scenes.

    OData Annotations

    The following sections explain how to add OData annotations to CDS models and how they’re mapped to EDMX outputs.

    Terms and Properties

    OData defines a strict two-fold key structure composed of @<Vocabulary>.<Term> and all annotations are always specified as a Term with either a primitive value, a record value, or collection values. The properties themselves may, in turn, be primitives, records, or collections.


    @Common.Label: 'Customer'
    @Common.ValueList: {
      Label: 'Customers',
      CollectionPath: 'Customers'
    entity Customers { }

    This is represented in CSN as follows:

        "kind": "entity",
        "@Common.Label": "Customer",
        "@Common.ValueList.Label": "Customers",
        "@Common.ValueList.CollectionPath": "Customers"

    And would render to EDMX as follows:

    <Annotations Target="MyService.Customers">
      <Annotation Term="Common.Label" String="Customer"/>
      <Annotation Term="Common.ValueList">
        <Record Type="Common.ValueListType">
          <PropertyValue Property="Label" String="Customers"/>
          <PropertyValue Property="CollectionPath" String="Customers"/>

    The value for @Common.ValueList is flattened to individual key-value pairs in CSN and ‘restructured’ to a record for OData exposure in EDMX.

    For each annotated target definition in CSN, the rules for restructuring from CSN sources are:

    1. Annotations with a single-identifier key are skipped (as OData annotations always have a @Vocabulary.Term... key signature).
    2. All individual annotations with the same @<Vocabulary.Term> prefix are collected.
    3. If there is only one annotation without a suffix, → that one is a scalar or array value of an OData term.
    4. If there are more annotations with suffix key parts →, it’s a record value for the OData term.

    Qualified Annotations

    OData foresees qualified annotations, which essentially allow to specify different values for a given property. CDS syntax for annotations was extended to also allow appending OData-style qualifiers after a # sign to an annotation key, but always only as the last component of a key in the syntax.

    For example, this is supported:

    @Common.Label: 'Customer'
    @Common.Label#Legal: 'Client'
    @Common.Label#Healthcare: 'Patient'
    @Common.ValueList: {
      Label: 'Customers',
    @Common.ValueList#Legal: {
      Label: 'Clients',

    and would render as follows in CSN:

      "@Common.Label": "Customer",
      "@Common.Label#Legal": "Clients",
      "@Common.Label#Healthcare": "Patients",
      "@Common.ValueList.Label": "Customers",
      "@Common.ValueList.CollectionPath": "Customers",
      "@Common.ValueList#Legal.Label": "Clients",
      "@Common.ValueList#Legal.CollectionPath": "Clients",

    Note that there’s no interpretation and no special handling for these qualifiers in CDS. You have to write and apply them in exactly the same way as your chosen OData vocabularies specify them.


    The @Some annotation isn’t a valid term definition. The following example illustrates the rendering of primitive values.

    Primitive annotation values, meaning Strings, Numbers, true, false, and null are mapped to corresponding OData annotations as follows:

    @Some.Null: null
    @Some.Boolean: true
    @Some.Integer: 1
    @Some.Number: 3.14
    @Some.String: 'foo'
    <Annotation Term="Some.Null"><Null/></Annotation>
    <Annotation Term="Some.Boolean" Bool="true"/>
    <Annotation Term="Some.Integer" Int="1"/>
    <Annotation Term="Some.Number" Decimal="3.14"/>
    <Annotation Term="Some.String" String="foo"/>

    Have a look at our CAP SFLIGHT sample, showcasing the usage of OData annotations.


    The @Some annotation isn’t a valid term definition. The following example illustrates the rendering of record values.

    Record-like source structures are mapped to <Record> nodes in EDMX, with primitive types translated analogously to the above:

    @Some.Record: {
      Null: null,
      Boolean: true,
      Integer: 1,
      Number: 3.14,
      String: 'foo'
    <Annotation Term="Some.Record">
        <PropertyValue Property="Null"><Null/></PropertyValue>
        <PropertyValue Property="Boolean" Bool="true"/>
        <PropertyValue Property="Integer" Int="1"/>
        <PropertyValue Property="Number" Decimal="3.14"/>
        <PropertyValue Property="String" String="foo"/>

    If possible, the type of the record in OData is deduced from the information in the OData Annotation Vocabularies:

    @Common.ValueList: {
      CollectionPath: 'Customers'
    <Annotation Term="Common.ValueList">
      <Record Type="Common.ValueListType">
        <PropertyValue Property="CollectionPath" String="Customers"/>

    Frequently, the OData record type cannot be determined unambiguously, for example if the type found in the vocabulary is abstract. Then you need to explicitly specify the type by adding a property named $Type in the record. For example:

    @UI.Facets : [{
      $Type  : 'UI.CollectionFacet',
      ID     : 'Customers'
    <Annotation Term="UI.Facets">
        <Record Type="UI.CollectionFacet">
          <PropertyValue Property="ID" String="Travel"/>

    There is one exception for a very prominent case: if the deduced record type is UI.DataFieldAbstract, the compiler by default automatically chooses UI.DataField:

    @UI.Identification: [{
      Value: deliveryId
    <Annotation Term="UI.Identification">
        <Record Type="UI.DataField">
          <PropertyValue Property="Value" Path="deliveryId"/>

    To overwrite the default, use an explicit $Type like shown previously.

    Have a look at our CAP SFLIGHT sample, showcasing the usage of OData annotations.


    The @Some annotation isn’t a valid term definition. The following example illustrates the rendering of collection values.

    Arrays are mapped to <Collection> nodes in EDMX and if primitives show up as direct elements of the array, these elements are wrapped into individual primitive child nodes of the resulting collection as is. The rules for records and collections are applied recursively:

    @Some.Collection: [
      true, 1, 3.14, 'foo',
      { $Type:'UI.DataField', Label:'Whatever', Hidden }
    <Annotation Term="Some.Collection">
        <Record Type="UI.DataField">
          <PropertyValue Property="Label" String="Whatever"/>
          <PropertyValue Property="Hidden" Bool="True"/>


    The @Some annotation isn’t a valid term definition. The following example illustrates the rendering of reference values.

    References in cds annotations are mapped to .Path properties or nested <Path> elements respectively:

    @Some.Term: My.Reference
    @Some.Record: {
      Value: My.Reference
    @Some.Collection: [
    <Annotation Term="Some.Term" Path="My/Reference"/>
    <Annotation Term="Some.Record">
        <PropertyValue Property="Value" Path="My/Reference"/>
    <Annotation Term="Some.Collection">

    Use a dynamic expression if the generic mapping can’t produce the desired <Path>:

    @Some.Term: {$edmJson: {$Path: '/'}}
    <Annotation Term="Some.Term">

    Enumeration Values

    Enumeration symbols are mapped to corresponding EnumMember properties in OData.

    Here are a couple of examples of enumeration values and the annotations that are generated. The first example is for a term in the Common vocabulary:

    @Common.TextFormat: #html
    <Annotation Term="Common.TextFormat" EnumMember="Common.TextFormatType/html"/>

    The second example is for a (record type) term in the Communication vocabulary:

    @Communication.Contact: {
      gender: #F
    <Annotation Term="Communication.Contact">
      <Record Type="Communication.ContactType">
        <PropertyValue Property="gender" EnumMember="Communication.GenderType/F"/>

    Annotating Annotations

    OData can annotate annotations. This often occurs in combination with enums like UI.Importance and UI.TextArrangement. CDS has no corresponding language feature. For OData annotations, nesting can be achieved in the following way:

    • To annotate a Record, add an additional element to the CDS source structure. The name of this element is the full name of the annotation, including the @. See @UI.Importance in the following example.
    • To annotate a single value or a Collection, add a parallel annotation that has the nested annotation name appended to the outer annotation name. See @UI.Criticality and @UI.TextArrangement in the following example.
    @UI.LineItem: [
        {Value: ApplicationName, @UI.Importance: #High},
        {Value: Description},
        {Value: SourceName},
        {Value: ChangedBy},
        {Value: ChangedAt}
    @UI.LineItem.@UI.Criticality: #Positive
    @Common.Text: Text
    @Common.Text.@UI.TextArrangement: #TextOnly

    Alternatively, annotating a single value or a Collection by turning them into a structure with an artificial property $value is still possible, but deprecated:

    @UI.LineItem: {
      $value:[ /* ... */ ], @UI.Criticality: #Positive
    @Common.Text: {
      $value: Text, @UI.TextArrangement: #TextOnly

    As TextArrangement is common, there’s a shortcut for this specific situation:

    @Common: {
      Text: Text, TextArrangement: #TextOnly

    In any case, the resulting EDMX is:

    <Annotation Term="UI.LineItem">
        <Record Type="UI.DataField">
          <PropertyValue Property="Value" Path="ApplicationName"/>
          <Annotation Term="UI.Importance" EnumMember="UI.ImportanceType/High"/>
      <Annotation Term="UI.Criticality" EnumMember="UI.CriticalityType/Positive"/>
    <Annotation Term="Common.Text" Path="Text">
      <Annotation Term="UI.TextArrangement" EnumMember="UI.TextArrangementType/TextOnly"/>

    Dynamic Expressions

    OData supports dynamic expressions in annotations. CDS syntax doesn’t allow writing expressions in annotation values, but for OData annotations you can use the “edm-json inline mechanism” by providing a dynamic expression as defined in the JSON representation of the OData Common Schema Language enclosed in { $edmJson: { ... }}.

    Note that here the CDS syntax for string literals with single quotes ('foo') applies, and that paths are not automatically recognized but need to be written as {$Path: 'fieldName'}. The CDS compiler translates the expression into the corresponding XML representation.

    For example, the CDS annotation:

    @UI.Hidden: {$edmJson: {$Ne: [{$Path: 'status'}, 'visible']}}

    is translated to:

    <Annotation Term="UI.Hidden">

    sap: Annotations

    In general, back ends and SAP Fiori UIs understand or even expect OData V4 annotations. You should use those rather than the OData V2 SAP extensions.

    If necessary, CDS automatically translates OData V4 annotations to OData V2 SAP extensions when invoked with v2 as the OData version. This means that you shouldn’t have to deal with this at all.

    Nevertheless, in case you need to do so, you can add sap:... attribute-style annotations as follows:

      @sap.applicable.path: 'to_eventStatus/EditEnabled'
      action EditEvent(...) returns SomeType;

    Which would render to OData EDMX as follows:

      <FunctionImport Name="EditEvent" ...

    The rules are:

    • Only strings are supported as values.
    • The first dot in @sap. is replaced by a colon :.
    • Subsequent dots are replaced by dashes.

    Differences to ABAP

    In contrast to ABAP CDS, we apply a generic, isomorphic approach where names and positions of annotations are exactly as specified in the OData Vocabularies. This has the following advantages:

    • Single source of truth — users only need to consult the official OData specs
    • Speed — we don’t need complex case-by-case mapping logic
    • No bottlenecks — we always support the full set of OData annotations
    • Bidirectional mapping — we can translate CDS to EDMX and vice versa

    Last but not least, it also saves us lots of effort as we don’t have to write derivatives of all the OData vocabulary specs.

    Annotation Vocabularies

    OASIS Vocabularies

    Vocabulary Description
    @Aggregation for describing aggregatable data
    @Authorization for authorization requirements
    @Capabilities for restricting capabilities of a service
    @Core for general purpose annotations
    @JSON for JSON properties
    @Measures for monetary amounts and measured quantities
    @Repeatability for repeatable requests
    @Temporal for temporal annotations
    @Validation for adding validation rules

    SAP Vocabularies

    Vocabulary Description
    @Analytics for annotating analytical resources
    @CodeList for code lists
    @Common for all SAP vocabularies
    @Communication for annotating communication-relevant information
    @DataIntegration for data integration
    @PDF for PDF
    @PersonalData for annotating personal data
    @Session for sticky sessions for data modification
    @UI for presenting data in user interfaces

    Learn more about annotations in CDS and OData and how they work together

    Data Aggregation

    Data aggregation in OData V4 is leveraged by the $apply system query option, which defines a pipeline of transformations that is applied to the input set specified by the URI. On the result set of the pipeline, the standard system query options come into effect. For data aggregation in OData V2, see Aggregation.


    GET /Orders(10)/books?
        $apply=filter(year eq 2000)/
               groupby((author/name),aggregate(price with average as avg))/

    This request operates on the books of the order with ID 10. First it filters out the books from the year 2000 to an intermediate result set. The intermediate result set is then grouped by author name and the price is averaged. Finally, the result set is sorted by title and only the top 3 entries are retained.


    Transformation Description Node.js Java
    filter filter by filter expression
    search filter by search term or expression n/a
    groupby group by dimensions and aggregates values
    aggregate aggregate values
    compute add computed properties to the result set n/a
    expand expand navigation properties n/a n/a
    concat append additional aggregation to the result (1)
    skip / top paginate (1)
    orderby sort the input set (1)
    topcount/bottomcount retain highest/lowest n values n/a n/a
    toppercent/bottompercent retain highest/lowest p% values n/a n/a
    topsum/bottomsum retain n values limited by sum n/a n/a
    • (1) Supported with experimental feature cds.features.odata_new_parser = true


    The concat transformation applies additional transformation sequences to the input set and concatenates the result:

    GET /Books?$apply=
        filter(author/name eq 'Bram Stroker')/
            aggregate($count as totalCount),
            groupby((year), aggregate($count as countPerYear)))

    This request filters all books, keeping only books by Bram Stroker. From these books, concat calculates (1) the total count of books and (2) the count of books per year. The result is heterogeneous.

    The concat transformation must be the last of the apply pipeline. If concat is used, then $apply can’t be used in combination with other system query options.

    skip, top, and orderby

    Beyond the standard transformations specified by OData, CDS Java supports the transformations skip, top, and orderby that allow you to sort and paginate an input set:

    GET /Order(10)/books?
        $apply=orderby(price desc)/
               groupby((author/name),aggregate(price with max as maxPrice))

    This query groups the 500 most expensive books by author name and determines the price of the most expensive book per author.

    Aggregation Methods

    Aggregation Method Description Node.js Java
    min smallest value
    max largest
    sum sum of values
    average average of values
    countdistinct count of distinct values
    custom method custom aggregation method n/a n/a
    $count number of instances in input set

    Custom Aggregates

    Instead of explicitly using an expression with an aggregation method in the aggregate transformation, the client can use a custom aggregate. A custom aggregate can be considered as a virtual property that aggregates the input set. It’s calculated on the server side. The client doesn’t know How the custom aggregate is calculated.

    They can only be used for the special case when a default aggregation method can be specified declaratively on the server side for a measure.

    A custom aggregate is declared in the CDS model as follows:

    • The measure must be annotated with an @Aggregation.default annotation that specifies the aggregation method.
    • The CDS entity should be annotated with an @Aggregation.CustomAggregate annotation to expose the custom aggregate to the client.
    @Aggregation.CustomAggregate#stock : 'Edm.Decimal'
    entity Books as projection on bookshop.Books{
      	@Aggregation.default: #SUM

    With this definition, it’s now possible to use the custom aggregate stock in an aggregate transformation:

    GET /Books?$apply=aggregate(stock) HTTP/1.1

    which is equivalent to:

    GET /Books?$apply=aggregate(stock with sum as stock) HTTP/1.1

    Currencies and Units of Measure

    If a property represents a monetary amount, it may have a related property that indicates the amount’s currency code. Analogously, a property representing a measured quantity can be related to a unit of measure. To indicate that a property is a currency code or a unit of measure it can be annotated with the Semantics Annotations @Semantics.currencyCode or @Semantics.unitOfMeasure.

    @Aggregation.CustomAggregate#amount   : 'Edm.Decimal'
    @Aggregation.CustomAggregate#currency : 'Edm.String'
    entity Sales {
        key id        : GUID;
            productId : GUID;
            @Semantics.amount.currencyCode: 'currency'
            amount    : Decimal(10,2);
            currency  : String(3);

    The CAP Java SDK exposes all properties annotated with @Semantics.currencyCode or @Semantics.unitOfMeasure as a custom aggregate with the property’s name that returns:

    • The property’s value if it’s unique within a group of dimensions
    • null otherwise

    A custom aggregate for a currency code or unit of measure should be also exposed by the @Aggregation.CustomAggregate annotation. Moreover, a property for a monetary amount or a measured quantity should be annotated with @Semantics.amount.currencyCode or @Semantics.quantity.unitOfMeasure to reference the corresponding property that holds the amount’s currency code or the quantity’s unit of measure, respectively.

    Other Features

    Feature Node.js Java
    use path expressions in transformations
    chain transformations
    chain transformations within group by n/a n/a
    groupby with rollup/$all n/a n/a
    $expand result set of $apply n/a n/a
    $filter/$search result set
    sort result set with $orderby
    paginate result set with $top/$skip


    A singleton is a special one-element entity introduced in OData V4. It can be addressed directly by its name from the service root without specifying the entity’s keys.

    Annotate an entity with @odata.singleton or @odata.singleton.nullable, to use it as a singleton within a service, for example:

    service Sue {
      @odata.singleton entity MySingleton {
        key id : String; // can be omitted
        prop : String;
        assoc : Association to myEntity;

    It can also be defined as an ordered SELECT from another entity:

    service Sue {
      @odata.singleton entity OldestEmployee as
        select from Employees order by birthyear;

    Requesting Singletons

    As mentioned above, singletons are accessed without specifying keys in the request URL. They can contain navigation properties, and other entities can include singletons as their navigation properties as well. The $expand query option is also supported.

    GET /MySingleton
    GET /MySingleton/prop
    GET /MySingleton/assoc
    GET /MySingleton?$expand=assoc

    Updating Singletons

    The following request updates a prop property of a singleton MySingleton:

    PATCH/PUT /MySingleton
    {prop: New value}

    Deleting Singletons

    A DELETE request to a singleton is possible only if a singleton is annotated with @odata.singleton.nullable. An attempt to delete a singleton annotated with @odata.singleton will result in an error.

    Creating Singletons

    Since singletons represent a one-element entity, a POST request is not supported.

    V2 Support

    While CAP defaults to OData V4, the latest protocol version, some projects need to fallback to OData V2, for example, to keep using existing V2-based UIs.

    Enabling OData V2 via Proxy in Node.js Apps

    CAP Node.js supports serving the OData V2 protocol through the OData V2 proxy protocol adapter, which translates between the OData V2 and V4 protocols.

    For Node.js projects, add the proxy as express.js middleware as follows:

    1. Add the proxy package to your project:

       npm add @sap/cds-odata-v2-adapter-proxy
    2. Add this to a project-local ./srv/server.js:

       const proxy = require('@sap/cds-odata-v2-adapter-proxy')
       const cds = require('@sap/cds')
       cds.on('bootstrap', app => app.use(proxy()))
       module.exports = cds.server
    3. Access OData V2 services at http://localhost:4004/v2/${path}.
    4. Access OData V4 services at http://localhost:4004/${path} (as before).

    Example: Read service metadata for CatalogService:

    • CDS:

        service CatalogService { ... }
    • OData V2: GET http://localhost:4004/v2/browse/$metadata
    • OData V4: GET http://localhost:4004/browse/$metadata

    Find detailed instructions at @sap/cds-odata-v2-adapter-proxy.

    Using OData V2 in Java Apps

    In CAP Java, serving the OData V2 protocol is supported natively by the CDS OData V2 Adapter.


    Omitting Elements from APIs

    Add annotation @cds.api.ignore to suppress unwanted entity fields (for example, foreign-key fields) in APIs exposed from this the CDS model, that is, OData or OpenAPI. For example:

    entity Books { ...
      author : Association to Authors;

    Please note that @cds.api.ignore is effective on regular elements that are rendered as Edm.Property only. The annotation doesn’t suppress an Edm.NavigationProperty which is rendered for associations or compositions. If a managed association is annotated, the annotations are propagated to the (generated) foreign keys. In the previous example, the foreign keys of the managed association author are muted in the API.

    Absolute Context URL

    In some scenarios, an absolute context URL is needed. In the Node.js runtime, this can be achieved through configuration cds.odata.contextAbsoluteUrl.

    You can use your own URL (including a protocol and a service path), for example:

    cds.odata.contextAbsoluteUrl = ""

    to customize the annotation as follows:

        {"ID": 201,"title": "Wuthering Heights","author": "Emily Brontë"},
        {"ID": 207,"title": "Jane Eyre","author": "Charlotte Brontë"},
        {"ID": 251,"title": "The Raven","author": "Edgar Allen Poe"}

    If contextAbsoluteUrl is set to something truthy that doesn’t match http(s)://*, an absolute path is constructed based on the environment of the application on a best effort basis.

    Note that we encourage you to stay with the default relative format, if possible, as it’s proxy safe.