The problem starts with the second board
With one MLS, the feed's field names become your schema by default and nothing looks wrong. The trouble arrives with the second board: a search for homes over 2,000 square feet returns nothing from one market because its area is in a differently named field, a status filter drops half the inventory, and the same house appears twice. The usual first fix is an if board == ... in the search code. By the fourth board there are dozens, and nobody can say where a given value on a listing page came from.
One of our published projects had this shape. The W3|re case study describes a brokerage across four MLS regions where each MLS used different RETS and Web API standards and schemas, and a pipeline that ingests from four MLS systems, normalizes schemas and deduplicates cross-listed properties.
Normalization is one stage of the MLS data pipeline reference architecture. Here it is taken apart: what the standard settles and what it leaves open, what the platforms convert for you, how to design the mapping layer and the canonical schema, and how to keep the source of every value.
A shared dictionary is not a shared dataset
The target is the RESO Data Dictionary. DD 2.0 is the current version for RESO certification, with 41 resources, 1,745 fields and 3,683 lookups; a 2.1 draft lists 43 resources, 2,139 fields and 4,129 lookups. Pin your canonical schema to a named version, because the next one adds fields.
The dictionary standardizes names, types and lookup values. It does not make every MLS populate every field. Its own pages publish an adoption figure for each one, as a share of 420 organizations, and the figures make the point:
| Field | Organizations using it (of 420) | What that means for a consumer |
|---|---|---|
| ListPrice | 100% | Safe to require |
| LivingArea | 79% | About one organization in five does not send it |
| LivingAreaUnits | 39% | The unit is often implied, not stated |
| SourceSystemName | 40% | You often have to record the immediate source yourself |
| MediaKey | 61% | Photo identity is not guaranteed |
| UniversalPropertyId | 21% | Cannot be the only basis for matching properties |
Those percentages are from Data Dictionary pages generated October 12, 2026. Consumers have said the same from experience. In a RESO article on moving to the Web API, the chief executive of an IDX vendor says that although field names were mostly consistent, the implementation of the data in those fields was not, and gives a listing's created date as an example: a plain date from one MLS, a timestamp in local server time from another, missing from a third.
What the platforms convert, and what they leave to you
Aggregation platforms do part of the work. How much is documented, and it is less than “normalized” suggests.
- MLS Grid converts names and lookups, not values. Its conversion rules only concern themselves with field names and, when applicable, Data Dictionary lookups. It keeps non-conforming fields, and local fields are prefixed to identify which MLS they originate from (the ACTRIS prefix is
ACT_, for example). Records are sparse: each contains only the fields converted from its originating MLS. - Trestle transforms to one structure and keeps the remainder separately. Its FAQ says it transforms data from each MLO into a consistent data structure and is certified for Data Dictionary 2.0. Data outside the standard is available by expanding other resources: its scaling guide shows expanding CustomProperty alongside Rooms, Units, OpenHouse and Media to get the custom data.
- Lookup values have two legal encodings. RESO allows a single-valued lookup to be either an OData enumeration or a string backed by the Lookup resource. In practice one feed sends
ActiveUnderContractand another sendsActive Under Contract. Trestle documents both forms and a PrettyEnums option that returns the human-friendly value. - Time zones are mixed inside one feed. On MLS Grid, date fields converted to a dictionary field are in UTC, while dates passed through as local fields keep their original representation. On Trestle, dates and timestamps are exposed in UTC, except that OpenHouse dates and times are in the time zone of the MLS.
So even through a platform, the consumer still owns value semantics, units, local fields, missing data and time zones. A direct feed from an MLS vendor may leave more.
The shape of the solution
The diagram as a list:
- Source schemas. Each board's feed arrives with its own field names, lookup values and units. In the illustration, Board A sends standard names, Board B sends a local status and a metric area, and Board C sends prefixed local fields with a number formatted as text.
- Raw store. Every payload is saved exactly as received, one row per source record, and never edited.
- Mapping layer. Versioned rules per source rename fields, map lookup values, convert units, cast types, keep local fields under a namespace and flag anything unmapped.
- Canonical schema. One listing shape that search, pages and APIs read, including the display flags that travel with the record.
- Provenance. Every canonical value records its source system and field, the raw record, the rule version and the transform. Because the raw record is kept, the rules can be re-run when they change.
Data flows one way, from raw to canonical, through rules that are configuration. Nothing downstream of the mapping layer should know which board a record came from, except to show attribution and apply that board's display rules.
An illustrative mapping
The table below is illustrative. The three boards are invented to show the kinds of difference a mapping layer has to absorb; none of the cells describes a real MLS.
| Canonical field | Board A (illustrative) | Board B (illustrative) | Board C (illustrative) | Rule |
|---|---|---|---|---|
standard_status | StandardStatus: Active | StandardStatus: ActiveUnderContract | XYZ_Status: A | Lookup map per source; unknown value is flagged, not guessed |
living_area_sqft | LivingArea: 1850, no units field | LivingArea: 172, LivingAreaUnits: Square Meters | XYZ_SqFtTotal: "1,850" | Unit from the field, else the source's declared default; convert; parse text |
lot_size_sqft | LotSizeSquareFeet: 8712 | LotSizeArea: 0.2, LotSizeUnits: Acres | LotSizeAcres: 0.2 | Prefer the explicit field; convert; keep the original pair |
property_sub_type | PropertySubType: Condominium | PropertySubType: Apartment | XYZ_Style: Condo/Co-op | Lookup map; one-to-many cases go to review |
bathrooms_total | BathroomsTotalInteger: 3 | BathroomsFull: 2, BathroomsHalf: 1 | XYZ_Baths: 2.5 | Keep full and half counts where known; never derive a count from a decimal silently |
photos[].position | Order starts at 0 | Order starts at 1 | No order; array position | Re-number from 1 per listing; keep the source order value |
show_address | InternetAddressDisplayYN: false | Field absent | XYZ_AddrDisp: N | Absent is not true; apply the source's documented default |
record_key | ListingKey | ListingKey with a platform prefix | ListingKey | Store as received; composite key with the originating system |
The sections below take the hard rows in turn, with what the standard actually says.
Identifiers
A listing arrives with several identifiers that are easy to confuse. The dictionary defines each one:
- ListingKey: a unique identifier for the record from the immediate source; the local key of the system.
- ListingId: the identifier a person uses. RESO warns that in a multiple originating system or a merged system, this value may not be unique.
- OriginatingSystemName: the name of the originating record provider, most commonly the name of the MLS.
- OriginatingSystemKey: the key from the system with authoritative control over the record, which may not be the system you received it from.
- SourceSystemName: the immediate record provider, the system from which the record was directly received.
The distinction between originating and source matters as soon as a platform sits between you and the MLS, or one MLS shares data with another. Our recommendation for the canonical key is the triple of feed (the connection you pulled from), originating system and record key. The feed is part of the key because the same listing can reach you through two licensed connections with different permitted uses, and those are different records for compliance purposes. ListingId is a display and lookup field, never a key.
Platforms also decorate keys: MLS Grid prefixes key fields, and the prefix should be removed before displaying externally but must be added back when requesting records. Store both forms.
Linking the same property across boards
One property listed in two MLSs is two legitimate records; the pipeline guide sets out why they are linked, never merged. The normalization layer's job is to produce the comparable values that matching needs:
- The universal identifier, where present. RESO created the Universal Parcel Identifier because property data is inconsistent when it relies upon addresses and standalone parcel numbers for identification. It is in version 2.0, with a URN-based model built from codes for the country and region assigning parcel numbers, the parcel numbers themselves, and subcomponents of the property. The dictionary field that carries it, UniversalPropertyId, shows usage by 21% of organizations, so treat it as strong evidence when both records have it and plan for its absence.
- A normalized parcel number. The dictionary notes that the assessor's parcel number format varies from county to county and recommends that parcel numbers be transmitted without dashes or hyphens. Not every source follows the recommendation, so strip punctuation into a comparison column and keep the original for display.
- A normalized address. Parsed components (number, direction, name, suffix, unit), a standard casing and a unit that is never dropped.
- Coordinates in one datum and one numeric type.
Property types and subtypes
Type is where a lookup map earns its keep. PropertyType is open with enumerations, with 9 standard values: sources may add their own. Usage of the standard values is uneven. The dictionary reports Residential at 98% of organizations and Land at 96%, but Residential Lease at 49%, Farm at 31% and Manufactured In Park at 6%. A board that does not use Residential Lease may still carry rentals, classified another way.
PropertySubType has 31 standard values with precise definitions that boards do not always share. The dictionary defines Apartment as a unit within a wholly owned structure of five or more units and Condominium as a unit within a structure where ownership is on a unit-by-unit basis, and notes that Apartment may not be used for Residential or Residential Income listings. A local value such as “Condo/Co-op” spans more than one standard value. Three rules for the map:
- Map local values to standard values per source, in data. Keep the local value beside the standard one.
- When one local value could be two standard values, do not pick. Send it to a review queue or map it to a deliberately broad canonical value.
- Search facets read the standard value; the listing page may show the board's own wording.
Units: area, lot size and currency
Living area. The dictionary pairs LivingArea, the total livable area within the structure, with LivingAreaUnits, whose standard values are Square Feet and Square Meters. Since far fewer organizations send the units field than the area field, the unit must often come from a declared per-source default. Record that the unit was assumed, and from which rule.
Lot size. The dictionary offers three representations: LotSizeArea with LotSizeUnits, whose standard values are Acres, Square Feet and Square Meters, plus the explicit LotSizeSquareFeet and LotSizeAcres. It says the explicit fields must be in sync with the values represented in Lot Size Area and Lot Size Units. A consumer should not assume they are. Choose a precedence, compute one canonical value, and flag records where the representations disagree beyond rounding. A flag of that kind is how you would catch, for example, a lot entered in acres in a square-foot field.
Currency. ListPrice is defined as the current price of the property as determined by the seller and the seller's broker: a number, with no currency in the definition. A platform serving both US and Canadian boards has to attach the currency from the originating system and store it on the record. Do not convert prices at ingestion; store the original amount and currency, and convert for display or comparison with a dated rate if the product needs it.
Counts that look like measures. BathroomsTotalInteger is the simple sum of the number of bathrooms: two full and one half is 3, and the dictionary says decimal-based bathrooms are not recommended, while allowing them in a non-standard field. A feed may therefore still send 2.5. The two numbers answer different questions, and a “3+ baths” filter gives different results depending on which was stored. Keep full and half counts as the canonical facts and derive any total from them.
Photos and media
Media rules (who may host photos, how URLs expire) are platform matters covered in the pipeline guide. Normalization has three smaller jobs:
- Order. The Data Dictionary says of Order that element zero is the primary photo per RESO convention. Trestle's metadata for the same field says element one is the primary photo, and element zero is an alias for element one. Both are documented; a consumer of several feeds must re-number positions itself and pick the primary photo by rule.
- Category. MediaCategory distinguishes photos from floor plans, documents, videos and virtual tours, but it is used by 45% of organizations. Where it is absent, classify by media type and keep the classification marked as inferred.
- Identity. Use the source's MediaKey where it exists. Where it does not, derive a stable identity from the listing key and the source URL or a content hash, so a re-sync does not download and store the same photo twice.
Missing and differently populated fields
Sparse records are normal, so the canonical schema has to distinguish three things that a nullable column merges: the source does not have this field, the source has it and sent no value, and the source sent a value we could not interpret. They lead to different behavior. A missing year built should exclude a listing from a “built after 2010” filter without implying it is old; an uninterpretable one should raise a mapping alert.
- Never write a default into a factual field. Zero bedrooms and unknown bedrooms are different listings.
- Keep a per-source field inventory: which canonical fields this source can populate at all. Search can then tell a user that a filter does not apply in a market, instead of silently returning nothing.
- Track fill rate per source and field over time. A drop in one field on one source is the earliest sign of a mapping or feed change.
Local fields deserve the opposite of neglect. Keep every one under a namespace for its source; promote a local field to the canonical schema only when a product feature needs it and at least a second source can supply the same fact.
Rules that travel with the data
Some fields are not facts about the property. They are instructions about display, and they must survive normalization with their meaning intact. The dictionary defines four seller-level flags:
- InternetEntireListingDisplayYN: whether the seller has allowed the listing to be displayed on Internet sites.
- InternetAddressDisplayYN: whether the seller has allowed the listing address to be displayed.
- InternetAutomatedValuationDisplayYN: whether the listing may be displayed with an automated valuation.
- InternetConsumerCommentYN: whether a comment or blog system may be attached to the listing.
Each has a matching rule in an MLS's display policy. In MLS Grid's IDX rules, for example, listings and addresses may be displayed except where a seller has directed that the listing or its address be withheld from public display, and a site that shows comments or an automated value estimate next to a listing must disable either feature for that listing at the seller's request. Platforms add their own: MLS Grid's MlgCanUse field indicates which use cases a record qualifies for.
Three recommendations for handling them:
- Do not map a missing flag to true. Decide the default per source from that MLS's documentation, and until it is decided, use the most restrictive reading.
- Enforce them in one filter before the index, not in page templates. An address that is in the search index can leak through autocomplete, a map pin or an API response even if the page hides it.
- Apply them to derived data. A hidden address also means no street-level map pin and no address in the page URL, title or structured data.
Status is the other field that carries rules; see handling deleted, expired and sold listings.
An illustrative canonical record with provenance
The example below is illustrative: the names, values and rule identifiers are invented.
// Illustrative canonical listing (one per feed + originating system + record key).
{
"feed_id": "feed-b",
"originating_system": "board-b",
"record_key": "B-20481",
"listing_id": "20481",
"standard_status": "ActiveUnderContract",
"mls_status": "ACT-UC",
"property_type": "Residential",
"property_sub_type": "Condominium",
"list_price": { "amount": 425000, "currency": "USD" },
"living_area_sqft": 1851,
"lot_size_sqft": 8712,
"bathrooms_full": 2,
"bathrooms_half": 1,
"display": { "listing": true, "address": false, "avm": true, "comments": true },
"local": { "board-b": { "ZoningOverlay": "R-2A" } },
"raw_ref": { "raw_id": "raw_9f31c2", "received_at": "2026-10-12T14:03:22Z", "payload_hash": "sha256:5d1e..." },
"mapping_version": "board-b@2026-10-01.3",
"provenance": {
"living_area_sqft": { "from": "LivingArea", "raw": 172, "unit": "Square Meters", "transform": "sqm_to_sqft" },
"lot_size_sqft": { "from": "LotSizeArea", "raw": 0.2, "unit": "Acres", "transform": "acres_to_sqft" },
"property_sub_type": { "from": "PropertySubType", "raw": "Condo", "transform": "lookup:board-b/subtype" },
"display.address": { "from": "InternetAddressDisplayYN", "raw": false, "transform": "identity" },
"list_price.currency": { "from": null, "transform": "source_default", "assumed": true }
},
"quality": { "unmapped_fields": [], "unmapped_values": [], "conflicts": [] }
}Why each part is there:
- The raw reference answers “what did the MLS actually send?” without a new download. The sync engine article covers how the raw store is written.
- The mapping version says which rules produced this record, so after a rule change you can find and re-process exactly the records made under the old one.
- Field-level provenance is kept for every field that was converted, mapped or assumed; identity copies can be implied by the mapping version to save space.
- The assumed marker separates what the source said from what you inferred. When an agent disputes a figure, that is the first thing support needs to know.
- The quality block records unmapped fields, unmapped lookup values and conflicts between representations, so they can be counted per source.
The search index is built from this record, not from the raw one. Real estate search with Elasticsearch covers the index mapping that sits on top.
Running the mapping layer
- Mapping rules are data in version control, one set per originating system, each with a version identifier written onto every record it produces.
- Each source has a small set of saved raw payloads with expected canonical output, run as tests on every rule change.
- Unmapped fields and unmapped lookup values are counted per source per run, and a rise alerts someone.
- Fill rate per source and field is charted; unit and representation conflicts are counted.
- A full re-normalization from the raw store is a routine job that has been rehearsed, with the index rebuilt after it.
- Adding a board means adding a source configuration and its tests. If it means editing search or page code, the boundary has leaked.
- Display flags and license scope have tests of their own: for each surface, a query for records it must not show returns zero.
Frequently Asked Questions
If every MLS uses the RESO Data Dictionary, why is normalization still needed?
Because the dictionary standardizes names, types and lookup values, not which fields each MLS fills or how. Its own pages show, for example, LivingAreaUnits used by 39% of organizations. Platforms also leave work: MLS Grid’s conversion rules only concern themselves with field names and, when applicable, Data Dictionary lookups.
Should local MLS fields be kept or dropped?
Kept, under a namespace for their source. MLS Grid already delivers them that way: local fields are prefixed to identify which MLS they originate from. A field dropped at ingestion has to be re-imported to add later; a field kept in the raw store and a local namespace costs little.
What should the primary key of a normalized listing be?
Not the listing number. RESO warns that in a merged system ListingId may not be unique. Use the originating system together with the record key, and add the feed where the same listing can reach you through more than one licensed connection.
How do you track where a normalized value came from?
Keep the raw payload unchanged, stamp every canonical record with the version of the mapping rules that produced it, and record field-level provenance (source field, value as received, transform) for anything converted, mapped or assumed.
What should happen when a display flag such as InternetAddressDisplayYN is missing?
Do not treat missing as permission. The field indicates whether the seller has allowed the listing address to be displayed on Internet sites. Set the default per source from that MLS’s documentation, and use the most restrictive reading until it is confirmed.
Data Dictionary definitions and usage figures, platform documentation and MLS Grid's IDX Rules were read from the linked pages on October 13, 2026; the Data Dictionary pages state that they were generated on October 12, 2026. Usage figures change as organizations certify. The mapping table, the board names and the canonical record are illustrative and describe no real MLS.
Adding a board, or untangling per-board conditions in an existing platform? See our MLS and IDX integration service. Every project starts with a 30-minute technical discovery call with an engineer.

