Betradar UOF Schemas
Ecto embedded schemas and a generic XML decoder for Betradar's Unified Odds Feed (UOF), generated from the official .NET SDK XSDs (pinned to v3.11.0). Schemas are namespaced under UOF.Schemas.API.* (HTTP API responses) and UOF.Schemas.Feed.* (AMQP feed messages).
Installation
Add :uof_schemas to your dependencies in mix.exs:
def deps do
[
{:uof_schemas, "~> 0.2.0"} # <!-- x-release-please-version -->
]
end
Usage
UOF.Schemas.XML.decode/1 is the everyday entry point. It dispatches on the document's root element against the full generated registry — every feed message and API response — and returns {:ok, struct}, {:error, Ecto.Changeset.t()}, or {:error, {:unknown_message, name}} for an unrecognised root. It walks the nested embeds and casts scalars (integers, decimals, datetimes, …) as it goes.
For the cases where you want to override that dispatch, decode/2 takes a schema module to decode a known type (static).
decode/1 — dispatch on the root element
You don't need to know which message you're holding. Hand the raw XML to decode/1 and it selects the matching schema — here an odds_change from the feed:
xml = """
<odds_change product="1" event_id="sr:match:11830662" timestamp="1620902980000">
<sport_event_status status="1" match_status="7" home_score="1" away_score="2"/>
<odds>
<market favourite="1" status="1" id="1">
<outcome id="1" odds="1.85" active="1" probabilities="0.524431"/>
<outcome id="2" odds="3.30" active="1" probabilities="0.286887"/>
<outcome id="3" odds="4.65" active="1" probabilities="0.188682"/>
</market>
</odds>
</odds_change>
"""
{:ok, odds_change} = UOF.Schemas.XML.decode(xml)
# => %UOF.Schemas.Feed.OddsChange{
# product: 1,
# event_id: "sr:match:11830662",
# timestamp: 1620902980000,
# sport_event_status: %UOF.Schemas.Feed.SportEventStatus{
# status: 1, match_status: 7,
# home_score: Decimal.new("1"), away_score: Decimal.new("2"), ...
# },
# odds: %UOF.Schemas.Feed.OddsChangeOdds{
# market: [
# %UOF.Schemas.Feed.OddsChangeMarket{
# id: 1, status: 1, favourite: 1,
# outcome: [
# %UOF.Schemas.Feed.OddsChangeMarketOutcome{id: "1", odds: Decimal.new("1.85"), probabilities: 0.524431, active: 1},
# %UOF.Schemas.Feed.OddsChangeMarketOutcome{id: "2", odds: Decimal.new("3.30"), probabilities: 0.286887, active: 1},
# %UOF.Schemas.Feed.OddsChangeMarketOutcome{id: "3", odds: Decimal.new("4.65"), probabilities: 0.188682, active: 1}
# ]
# }
# ]
# }, ...
# }
Any HTTP API response decodes the same way. Every endpoint can also return the shared <response> envelope (an error or a write acknowledgement), which decode/1 routes to UOF.Schemas.Common.Response:
{:ok, response} =
UOF.Schemas.XML.decode(~s(<response response_code="NOT_FOUND"><message>no such event</message></response>))
# => %UOF.Schemas.Common.Response{response_code: "NOT_FOUND", message: "no such event", ...}
decode/2 — override the dispatch
When the endpoint already fixes the type, pass the schema module directly to assert it regardless of the root element. For example, the market descriptions endpoint (GET /v1/descriptions/{language}/markets.xml):
xml = """
<market_descriptions response_code="OK">
<market id="1" name="1x2" groups="all|score|regular_play">
<outcomes>
<outcome id="1" name="{$competitor1}"/>
<outcome id="2" name="draw"/>
<outcome id="3" name="{$competitor2}"/>
</outcomes>
</market>
</market_descriptions>
"""
{:ok, descriptions} =
UOF.Schemas.XML.decode(xml, UOF.Schemas.API.Descriptions.MarketDescriptions)
# => %UOF.Schemas.API.Descriptions.MarketDescriptions{
# response_code: "OK",
# market: [
# %UOF.Schemas.API.Descriptions.DescMarket{
# id: 1, name: "1x2", groups: "all|score|regular_play",
# outcomes: %UOF.Schemas.API.Descriptions.DescOutcomes{
# outcome: [
# %UOF.Schemas.API.Descriptions.DescOutcomesOutcome{id: "1", name: "{$competitor1}"},
# %UOF.Schemas.API.Descriptions.DescOutcomesOutcome{id: "2", name: "draw"},
# %UOF.Schemas.API.Descriptions.DescOutcomesOutcome{id: "3", name: "{$competitor2}"}
# ]
# }, ...
# }
# ]
# }
Regenerating schemas
The modules under lib/uof/schemas/ are generated and committed, so consumers never run codegen. To refresh them against the pinned SDK tag:
mix uof.schemas.gen # fetch XSDs (if missing) + generate
mix format # apply Styler to the generated modules
mix uof.schemas.gen is additive and only fetches when the cache is empty. To force a clean refresh (re-download XSDs and drop removed/renamed modules), run make clean first — it deletes priv/xsd/ and the generated schema directories — then regenerate:
make clean && mix uof.schemas.gen && mix format
The upstream repo and pinned tag live in codegen/xsd/sources.ex.