What SAP EDMX Metadata Actually Contains
What $metadata Returns
Every SAP OData service exposes a $metadata endpoint. Hit /sap/opu/odata/sap/API_BUSINESS_PARTNER/$metadata and you get back an XML document — an EDMX file — that describes the entire service: every entity type, every property with its data type and constraints, every navigation relationship between entities, every entity set exposed in the service container.
The EDMX document is a machine-readable API contract — the most complete description of a SAP OData service's structure that exists outside the ABAP source code.
This post walks through a real EDMX document section by section. Every XML snippet comes from the API_BUSINESS_PARTNER EDMX metadata exported from an S/4HANA system. The goal is to explain what each section contains, what it tells you, and — equally important — what it does not.
The Outer Structure
An EDMX document wraps everything in an edmx:Edmx root element with a DataServices child. Inside DataServices is one or more Schema elements that hold the actual type definitions:
<edmx:Edmx Version="1.0"
xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns:sap="http://www.sap.com/Protocols/SAPData">
<edmx:DataServices m:DataServiceVersion="2.0">
<Schema Namespace="API_BUSINESS_PARTNER"
xml:lang="en" sap:schema-version="1"
xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<!-- EntityTypes, Associations, EntityContainer, Annotations -->
</Schema>
</edmx:DataServices>
</edmx:Edmx>
The Namespace attribute on the Schema element (API_BUSINESS_PARTNER) is the prefix used throughout the document for fully qualified type references. When a property elsewhere says Type="API_BUSINESS_PARTNER.A_BusinessPartnerType", it is referencing a type defined in this schema.
Before the DataServices block, SAP EDMX files include edmx:Reference elements that import vocabulary definitions — OData capability annotations, SAP-specific annotation vocabularies like com.sap.vocabularies.Common.v1 and com.sap.vocabularies.PersonalData.v1, and standard OData vocabularies like Org.OData.Capabilities.V1. These references are used later in the Annotations section to attach metadata to entity sets and properties.
EntityType: The Type Definition
An EntityType defines the shape of an entity — its key fields and all its properties. Here is a trimmed version of A_BusinessPartnerType:
<EntityType Name="A_BusinessPartnerType"
sap:label="Business Partner" sap:content-version="1">
<Key>
<PropertyRef Name="BusinessPartner"/>
</Key>
<Property Name="BusinessPartner" Type="Edm.String"
Nullable="false" MaxLength="10"
sap:display-format="UpperCase"
sap:label="Business Partner"
sap:quickinfo="Business Partner Number"/>
<Property Name="Customer" Type="Edm.String"
MaxLength="10" sap:display-format="UpperCase"
sap:label="Customer" sap:quickinfo="Customer Number"
sap:creatable="false" sap:updatable="false"/>
<Property Name="BusinessPartnerFullName" Type="Edm.String"
MaxLength="81"
sap:creatable="false" sap:updatable="false"/>
<Property Name="CreationDate" Type="Edm.DateTime"
Precision="0" sap:display-format="Date"
sap:label="Created On"
sap:creatable="false" sap:updatable="false"/>
<Property Name="BusinessPartnerUUID" Type="Edm.Guid"
sap:label="BP GUID"
sap:creatable="false" sap:updatable="false"/>
<Property Name="IsFemale" Type="Edm.Boolean"
sap:label="Female"
sap:quickinfo="Selection: Business partner is female"/>
<!-- ... 64 more properties, 26 navigation properties -->
</EntityType>
A few things to notice:
Key fields. The <Key> block declares which properties form the entity key. For A_BusinessPartnerType, it is a single property: BusinessPartner. Other entity types have composite keys — A_AddressEmailAddressType has a three-part key (AddressID, Person, OrdinalNumber).
Edm types. Each property has a Type attribute using the OData Entity Data Model type system. The common types you will encounter in SAP EDMX are Edm.String, Edm.Boolean, Edm.DateTime, Edm.Decimal, Edm.Guid, Edm.Int32, and Edm.Time. String properties dominate because SAP stores most identifiers, codes, and descriptive fields as fixed-length character strings.
Nullable. When Nullable="false" is present, the property is required. When the attribute is absent, the property is nullable. This is a common source of confusion: the absence of Nullable does not mean "required" — it means "nullable." The OData specification defines the default as true.
MaxLength and Precision. String properties carry MaxLength (e.g., MaxLength="10" for BusinessPartner). Decimal properties carry Precision and Scale (e.g., Precision="16" Scale="3"). DateTime properties carry Precision="0" indicating no fractional seconds.
SAP-Specific Annotations on Properties
Standard OData defines Name, Type, Nullable, MaxLength, Precision, Scale. SAP extends this with a sap: namespace that adds operational metadata:
sap:label — A human-readable label for the property. sap:label="Business Partner" is what appears in SAP Fiori UIs and reports.
sap:quickinfo — A longer description. sap:quickinfo="Business Partner Number" provides context that sap:label alone does not.
sap:display-format — Controls how the value is rendered. UpperCase means the value is stored and displayed in uppercase. NonNegative means the numeric string value should not display a leading minus sign. Date on an Edm.DateTime property means the time portion is always zero — it is a date-only field despite the type being Edm.DateTime.
sap:creatable="false" and sap:updatable="false" — Marks a property as read-only for create or update operations. Customer on A_BusinessPartnerType has both set to false: it is a derived field, populated by SAP when the business partner is linked to a customer master record. You cannot set it via the OData API.
sap:unit — Links a numeric property to its unit of measure or currency property. For example, a decimal amount property annotated with sap:unit="BPBalanceSheetCurrency" is denominated in whatever currency that referenced property holds.
sap:semantics — Provides semantic meaning beyond the data type. In API_BUSINESS_PARTNER, all usages are sap:semantics="currency-code", identifying properties like BPBalanceSheetCurrency and PurchaseOrderCurrency as ISO 4217 currency code fields. Other SAP services use values like unit-of-measure for quantity unit properties.
These annotations are not part of the OData standard. They are SAP extensions in the http://www.sap.com/Protocols/SAPData namespace. Tools that parse EDMX without the SAP namespace will ignore them silently.
NavigationProperty: Relationships Between Entities
Inside an EntityType, NavigationProperty elements declare how this entity relates to other entities:
<NavigationProperty Name="to_BusinessPartnerAddress"
Relationship="API_BUSINESS_PARTNER.assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"
FromRole="FromRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"
ToRole="ToRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"/>
<NavigationProperty Name="to_BusinessPartnerBank"
Relationship="API_BUSINESS_PARTNER.assoc_05F0DA10FFAB2836605571E5ABB6E5B9"
FromRole="FromRole_assoc_05F0DA10FFAB2836605571E5ABB6E5B9"
ToRole="ToRole_assoc_05F0DA10FFAB2836605571E5ABB6E5B9"/>
<NavigationProperty Name="to_Customer"
Relationship="API_BUSINESS_PARTNER.assoc_80838433DCE8E16AA7C9031C32896631"
FromRole="FromRole_assoc_80838433DCE8E16AA7C9031C32896631"
ToRole="ToRole_assoc_80838433DCE8E16AA7C9031C32896631"/>
The Name attribute (to_BusinessPartnerAddress) is what you use in OData queries with $expand. The Relationship attribute is a reference to an Association element defined elsewhere in the EDMX — that is where the actual type mapping lives.
Navigation properties form a graph. A_BusinessPartnerType navigates to A_BusinessPartnerAddressType via to_BusinessPartnerAddress. A_BusinessPartnerAddressType in turn has its own navigation properties: to_EmailAddress, to_PhoneNumber, to_FaxNumber, to_URLAddress, and 4 others. This is how multi-level $expand paths like to_BusinessPartnerAddress/to_EmailAddress work — each level resolves through the target entity type's own navigation properties.
Association: The Type-Level Relationship
Each NavigationProperty references an Association, which defines the two entity types involved and the multiplicity (one-to-one, one-to-many):
<Association Name="assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"
sap:content-version="1">
<End Type="API_BUSINESS_PARTNER.A_BusinessPartnerType"
Multiplicity="1"
Role="FromRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"/>
<End Type="API_BUSINESS_PARTNER.A_BusinessPartnerAddressType"
Multiplicity="*"
Role="ToRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"/>
</Association>
This says: one A_BusinessPartnerType has zero or more (*) A_BusinessPartnerAddressType entities. The multiplicity values are 1, 0..1, or *. This is the only place in the EDMX where the relationship cardinality is explicitly stated.
SAP generates association names as hash-based identifiers (e.g., assoc_6247ED959B75350E6EA6CEDD2CBEC7E7). These are not human-readable. The only way to understand what an association connects is to look at its End elements.
AssociationSet: The Container-Level Binding
An AssociationSet maps an Association to specific EntitySet elements inside the EntityContainer. It answers: "when I traverse this navigation property, which entity set do I land in?"
<AssociationSet Name="assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"
Association="API_BUSINESS_PARTNER.assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"
sap:creatable="false" sap:updatable="false"
sap:deletable="false" sap:content-version="1">
<End EntitySet="A_BusinessPartner"
Role="FromRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"/>
<End EntitySet="A_BusinessPartnerAddress"
Role="ToRole_assoc_6247ED959B75350E6EA6CEDD2CBEC7E7"/>
</AssociationSet>
The AssociationSet also carries its own SAP annotations — sap:creatable="false", sap:updatable="false", sap:deletable="false" — indicating that this relationship cannot be created, modified, or deleted through the OData API. You can read the addresses of a business partner, but you cannot create a new address-to-partner link via this navigation path.
The distinction between Association (schema level) and AssociationSet (container level) is a design pattern from OData V2. In OData V4, this was simplified: associations and association sets were replaced by NavigationPropertyBinding elements directly in the EntityContainer. Most SAP S/4HANA APIs still use V2, so the V2 pattern is what you will encounter.
EntitySet: What the URL Exposes
The EntityContainer holds EntitySet elements — these are the actual endpoints you can query via HTTP:
<EntityContainer Name="API_BUSINESS_PARTNER_Entities"
m:IsDefaultEntityContainer="true"
sap:message-scope-supported="true"
sap:supported-formats="atom json xlsx">
<EntitySet Name="A_BusinessPartner"
EntityType="API_BUSINESS_PARTNER.A_BusinessPartnerType"
sap:deletable="false" sap:content-version="1"/>
<EntitySet Name="A_BusinessPartnerAddress"
EntityType="API_BUSINESS_PARTNER.A_BusinessPartnerAddressType"
sap:content-version="1"/>
<EntitySet Name="A_Customer"
EntityType="API_BUSINESS_PARTNER.A_CustomerType"
sap:creatable="false" sap:deletable="false"
sap:content-version="1"/>
<!-- ... 62 more EntitySets -->
</EntityContainer>
Each EntitySet maps a URL-accessible name to an EntityType. When you call GET /sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner, you are addressing the A_BusinessPartner entity set, which returns entities shaped according to A_BusinessPartnerType.
This is the EntityType-vs-EntitySet naming mismatch that causes confusion. The URL uses A_BusinessPartner (the EntitySet name). The entity in the response body carries "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType" (the namespace-qualified EntityType name). These are defined in different parts of the EDMX, and they do not always follow a predictable naming convention. In this service, the pattern is EntitySet A_X maps to EntityType A_XType — but this is a convention SAP happens to follow for API_BUSINESS_PARTNER, not a rule enforced by the OData specification.
The EntitySet also carries operational annotations. A_BusinessPartner has sap:deletable="false" — you cannot issue a DELETE against business partner entities through this API. A_Customer has both sap:creatable="false" and sap:deletable="false" — it is read-only with update support. These annotations describe what the OData service allows, not what the underlying SAP authorization model enforces.
FunctionImport: Custom Operations
Not all SAP OData services are limited to CRUD on entity sets. FunctionImport elements define custom operations. API_BUSINESS_PARTNER does not have any, but API_SALES_ORDER_SRV does:
<FunctionImport Name="rejectApprovalRequest"
ReturnType="API_SALES_ORDER_SRV.FunctionResult"
m:HttpMethod="POST">
<Parameter Name="SalesOrder" Type="Edm.String"
Mode="In" MaxLength="11000"/>
</FunctionImport>
This declares a POST-based operation named rejectApprovalRequest that accepts a SalesOrder string parameter and returns a FunctionResult complex type. You call it as POST /sap/opu/odata/sap/API_SALES_ORDER_SRV/rejectApprovalRequest?SalesOrder='500000001'.
FunctionImports are SAP's mechanism for exposing business operations that do not map cleanly to entity CRUD — approval workflows, document posting, PDF generation. API_PURCHASEORDER_PROCESS_SRV defines GetPDF and GetOutputBinaryData for retrieving purchase order printouts.
Annotations: Capabilities and Restrictions
After the EntityContainer, the EDMX contains Annotations blocks that attach OData Vocabulary annotations to entity sets and their properties:
<Annotations
Target="API_BUSINESS_PARTNER.API_BUSINESS_PARTNER_Entities/A_BPAddressIndependentWebsite"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<Annotation Term="Capabilities.FilterRestrictions">
<Record>
<PropertyValue Property="FilterExpressionRestrictions">
<Collection>
<Record>
<PropertyValue Property="Property"
PropertyPath="WebsiteURL"/>
<PropertyValue Property="AllowedExpressions"
String="SearchExpression"/>
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
<Annotation Term="Capabilities.SortRestrictions">
<Record>
<PropertyValue Property="NonSortableProperties">
<Collection>
<PropertyPath>WebsiteURL</PropertyPath>
</Collection>
</PropertyValue>
</Record>
</Annotation>
</Annotations>
This says: on the A_BPAddressIndependentWebsite entity set, the WebsiteURL property only supports search expressions for filtering (not arbitrary $filter comparisons), and it cannot be used in $orderby. These annotations use the Org.OData.Capabilities.V1 vocabulary imported in the edmx:Reference block at the top of the document.
These annotations encode operational constraints that are otherwise only discoverable by trial and error against a live system — or by reading ABAP source code.
What EDMX Does Not Tell You
The EDMX describes structure. It does not describe behavior. Specifically:
No business validation rules. The EDMX can tell you that BusinessPartnerCategory is an Edm.String with MaxLength="1". It cannot tell you that the only valid values are 1 (Person), 2 (Organization), and 3 (Group). Domain-level value constraints live in SAP's ABAP data dictionary, not in the OData metadata.
No authorization scoping. The sap:creatable, sap:updatable, and sap:deletable annotations describe the service's general CRUD capabilities. They do not reflect per-user authorization. A property marked sap:updatable="true" may still reject your update if your user lacks the required SAP authorization object.
No cross-entity business logic. The EDMX tells you that A_BusinessPartnerType has a navigation property to_Customer. It does not tell you what happens inside SAP when you create or modify entities through that relationship — any downstream processing, validations, or side effects are invisible in the metadata.
No data volume hints. Nothing in the EDMX indicates whether an entity set contains 50 records or 50 million. This matters for queries without $top — some entity sets will time out or hit SAP's response size limits if you request them unfiltered.
No inter-environment differences. The EDMX describes one environment's state at the time it was generated. The same API_BUSINESS_PARTNER service on a DEV system may have different custom fields, different annotations, or different entity types than on QAS or PRD — SAP metadata reflects the system's configuration, including custom extensions.
Common EDMX Gotchas
EntityType name vs EntitySet name. As shown above, these are defined separately and do not always follow the same pattern. The EntitySet name appears in URLs. The EntityType name appears in response __metadata.type fields. Code that constructs URLs from EntityType names, or validates response types against EntitySet names, will break on services where the naming convention differs.
Missing Nullable attribute. In the OData specification, Nullable defaults to true when absent. This means every property without an explicit Nullable="false" is nullable. Code generators that treat the absence of Nullable as "required" will produce wrong type definitions — they will generate non-optional fields for properties that can be null.
sap:display-format="Date" on Edm.DateTime. SAP frequently uses Edm.DateTime for date-only fields, relying on sap:display-format="Date" to signal that the time portion is always zero. Any code that parses these values and preserves the time component will produce incorrect results — the T00:00:00 is not meaningful. In OData V4, SAP migrated some of these to Edm.Date, but V2 services will carry this pattern indefinitely.
Hash-based association names. SAP generates association names as 32-character hexadecimal hashes (e.g., assoc_6247ED959B75350E6EA6CEDD2CBEC7E7). These are opaque. Code that logs or displays association names for debugging purposes will produce unreadable output. The only useful names are on the NavigationProperty elements themselves.
Read-only annotations at multiple levels. A property can be marked sap:creatable="false" and sap:updatable="false" on the Property element, while the containing EntitySet might be marked sap:deletable="false", and the AssociationSet might carry its own sap:creatable="false". These annotations operate independently. A property might be updatable on its own, but the entity set it belongs to might not be deletable, and the association set linking to it might not be creatable. To determine the full CRUD capability for a specific access path, you need to check all three levels.
Four Things You Can Build on EDMX
1. Auto-generate typed client code. OData2Poco reads an EDMX document and generates C# POCO classes — one class per entity type, with properties matching the EDMX definitions, including key attributes, required attributes, and data annotations. It supports OData V1-V4. For TypeScript, odata2ts generates TypeScript interfaces from EDMX, producing query response models, edit models, and entity ID types. Both tools eliminate the manual work of translating EDMX definitions into application-layer type definitions. When the EDMX changes, you regenerate.
2. Validate API responses against the schema. If you have the EDMX, you can write assertions that verify a response from SAP (or from any mock) conforms to the schema: correct property names, correct data types, no extra or missing fields. This turns the EDMX into a contract test. Schema drift — where the actual API response has diverged from what your code expects — becomes detectable in CI rather than in production.
3. Detect schema drift between environments. Export $metadata from DEV, QAS, and PRD. Diff the EDMX documents. Differences reveal custom fields added in one environment but not transported to another, entity types present in DEV that have not been activated in PRD, or annotation changes from a support package applied to QAS but not yet to DEV. This is a lightweight alternative to comparing ABAP transport requests.
4. Generate mock responses for testing. A tool that reads the EDMX knows every entity type, every property's data type, and every navigation relationship. That is enough to produce structurally correct OData responses — with proper envelopes, /Date(…)/ encoding, and nested $expand results — without hand-written stub files. MockLayer does exactly this. See How to Mock SAP OData Services in Your CI/CD Pipeline for the full setup.