Understanding our data model

In this article, we will describe the way we represent product data at SupplierXM. We will walk you through all the types of fields we have, with data samples for each of them. Our data model can be expressed either in JSON or in XML (JSON being the data language we recommend).

Our APIs use UTF-8 encoding for both reading and writing.

1. Simple types

a. Integers (positive integer, negative integer, integer)

{
  "simpleIntegerField1": 1234,
  "simpleIntegerField2": -4321
}
<Product>
  <simpleIntegerField1>1234</simpleIntegerField1>
  <simpleIntegerField2>-4321</simpleIntegerField2>
</Product>

b. Floats (positive float, negative float, float)

These fields accept both integer and non-integer decimal numbers.

{
  "simpleFloatField1": 1234,
  "simpleFloatField2": -56.78
}
<Product>
  <simpleFloatField1>1234</simpleFloatField1>
  <simpleFloatField2>-56.78</simpleFloatField2>
</Product>

c. Booleans (boolean)

Booleans can only have two values: either true or false.

{
  "booleanField1": true,
  "booleanField2": false
}
<Product>
  <booleanField1>TRUE</booleanField1>
  <booleanField2>FALSE</booleanField2>
</Product>

d. Dates (date, iso_date)

Dates are represented in the form of the UNIX timestamp (in second) of a given day at 00:00 UTC.
ISO dates are represented as strings of the format "YYYY-mm-dd".

{
  "dateField": 1536710400, // converts to September 12th, 2018
  "isoDateField": "2019-08-06" // converts to August 6th, 2019
}
<Product>
  <dateField>1536710400</dateField> <!--converts to September 12th, 2018-->
  <isoDateField>2019-08-06<isoDateField> <!--converts to August 6th, 2019-->
</Product>

e. Strings (string, text, gln)

String fields contain textual data. They have no constraint on the characters they may contain. However, we strongly recommend you not use control characters (unicode 0000 - 001F, except tabs, new lines and carriage returns, and unicode 0080 - 009F). Each string field may have a different maximum length, please refer to the attributes documentation.

GLNs are string, with a special validation: the data must be a valid GLN (13 characters and valid check digit).

{
  "simpleString1": "I am a small text",
  "simpleString2": "I am a more elaborate text.\nI contain several lines.",
  "simpleGLN": "3663215000011"
}
<Product>
  <simpleString1>I am a small text</simpleString1>
  <simpleString2>I am a more elaborate text.
I contain several lines.<simpleString2>
  <simpleGLN>3663215000011</simpleGLN>
</Product>

f. String lists (string_list)

These fields contain a list of strings.

{
  "stringListField": [
    "string1",
    "string2",
    "string3" 
  ]
}
<Product>
  <stringListField>
        <stringListField>string1</stringListField>
        <stringListField>string2</stringListField>
        <stringListField>string3</stringListField>
  <stringListField>
</Product>

2. Entities (code list, kind, brand)

A lot of our fields can contain only a specific list of values. We call one of these values an "entity", and the full list of acceptable values is called a "referential".

For example, a simple referential "colors" could contain the following entities:

code

label (english)

Description (english)

RED

Red

The color red.

BLUE

Blue

The color blue.

GREEN

Green

The color green.

WHITE

White

The color white.

Entity fields must then contain one of these values. We reference the code to represent it:

{
  "colorEntityField": {
    "code": "WHITE"
  }
}
<Product>
  <colorEntityField>WHITE</colorEntityField>
</Product>

It is crucial that the code actually exists in the referential linked to the field, otherwise the data will be rejected. Please refer to the attribute documentation for the list of available codes.

Access all possible values for an entity in the attribute documentation.Access all possible values for an entity in the attribute documentation.

Access all possible values for an entity in the attribute documentation.

📘

When reading the JSON API, entities will be serialized with several attributes, such as the label or the description. However, these attributes are expected to change over time, the only constant attribute is the code. When pushing data to SupplierXM, only the code will be taken into account.

📘

Kind is an entity of the referential kinds.
Brand is like an entity, but instead of refering to a referential, it refers to the list of the brands your organization owns.

3. Products (product)

These fields reference another product in your organization's catalog. The identifier is the GTIN.

{
  "productField": {
    "gtin": "01234567890005"
  }
}
<Product>
  <productField>
    <gtin>01234567890005</gtin>
  </productField>
</Product>

4. Declinable fields

Every one of the previous fields can, on top of its type, be declinable by a certain referential. Basically, it means that for each entity in the referential, you can associate a value of the type of your field. We represent this with a list of objects, in which the value is found in "data", and the entity is in "expressedIn".

Let's see a few common examples.

a. String declinable by lang

The most common case of declinable field is for a localized text.

{
  "localizedText": [{  // declinable by languages
    "data": "My text in english",
    "expressedIn": {
      "code": "eng-GB"  // entity of referential languages
    }
  }, {
    "data": "Mon texte en français",
    "expressedIn": {
      "code": "fra-FR"  // entity of referential languages
    }
  }, {
    "data": "Mi texto en español",
    "expressedIn": {
      "code": "spa-ES"  // entity of referential languages
    }
  }]
}
<Product>
  <!-- <fieldName referentialName="entity code">data</fieldName> -->
  <localizedText languages="eng-GB">My text in english<localizedText>
  <localizedText languages="fra-FR">Mon texte en français<localizedText>
  <localizedText languages="spa-ES">Mi texto en español<localizedText>
</Product>

❗️

You can only express one value for each entity, e.g. this is forbidden:

{
  "localizedText": [{
    "data": "My text in english",
    "expressedIn": {
      "code": "eng-GB"  // language for the first value
    }
  }, {
    "data": "Some other value in english",
    "expressedIn": {
      "code": "eng-GB"  // same language as the previous element: forbidden
    }
  }
}
<Product>
  <localizedText languages="eng-GB">My text in english<localizedText>
  <localizedText languages="eng-GB">Some other value in english<localizedText>
  <!-- same language as the previous element: forbidden -->
</Product>

b. Float declinable by unit of measure

Another common case is a field representing a quantity associated with a unit of measure. You can express your quantity in several units, if needed.

{
  "netWeight": [{
    "data": 17.3,
    "expressedIn": {
      "code": "kg"
    }
  }, {
    "data": 38.14,
    "expressedIn": {
      "code": "lb"
    }
  }]
}
<Product>
  <netWeight weights="kg">17.3</netWeight>
  <netWeight weights="lb">38.14</netWeight>
</Product>

5. Dictionaries (dict)

Dictionaries are a more complex type of fields. They encapsulate a group of other fields, which can be of any type.

{
  "batteries": {
    "areBatteriesIncluded": false,
    "areBatteriesRequired": true
  }
}
<Product>
  <batteries>
    <areBatteriesIncluded>FALSE</areBatteriesIncluded>
    <areBatteriesRequired>TRUE</areBatteriesRequired>
  </batteries>
</Product>

6. Lists (list)

Lists represent repeatable elements. They can only contain dictionaries (cf. previous paragraph). For example, the following example describes two temperatures:

  • Delivery to distribution centre (DDC), minimum 7°C (44.6°F), maximum 70°C (158°F)
  • Storage (SH), minimum 17°C (value in °F not specified), maximum 45°C (value in °F not specified)
{
    "temperature": [  // start of the list
        {  // start of the first element
            "min": [  // min for first element (float declinable by temperatures)
                {
                    "data": 7,
                    "expressedIn": {
                        "code": "°C"
                    }
                },
                {
                    "data": 44.6,
                    "expressedIn": {
                        "code": "°F"
                    }
                }
            ],
            "max": [  // max for first element (float declinable by temperatures)
                {
                    "data": 70,
                    "expressedIn": {
                        "code": "°C"
                    }
                },
                {
                    "data": 158,
                    "expressedIn": {
                        "code": "°F"
                    }
                }
            ],
            "qualifier": {  // qualifier for first element
                "code": "DDC"
            }
        },
        {  // start of the second element
            "min": [  // min for second element (float declinable by temperatures)
                {
                    "data": 17,
                    "expressedIn": {
                        "code": "°C"
                    }
                }
            ],
            "max": [  // max for second element (float declinable by temperatures)
                {
                    "data": 45,
                    "expressedIn": {
                        "code": "°C"
                    }
                }
            ],
            "qualifier": {  // qualifier for second element
                "code": "SH"
            }
        }
    ]
}
<Product>
  <temperature>  <!-- start of the list -->
    <temperature>  <!-- start of the first element -->
      <min temperatures="°C">7</min>  <!-- min for the first element in °C -->
      <min temperatures="°F">44.6</min>  <!-- min for the first element in °F -->
      <max temperatures="°C">70</max>  <!-- max for the first element in °C -->
      <max temperatures="°F">158</max>  <!-- max for the first element in °F -->
      <qualifier>DDC</qualifier>  <!-- qualifier for the first element -->
    </temperature>  <!-- end of the first element -->
    <temperature>  <!-- start of the second element -->
      <min temperatures="°C">17</min>  <!-- min for the second element in °C -->
      <max temperatures="°C">45</max>  <!-- max for the second element in °C -->
      <qualifier>SH</qualifier>  <!-- qualifier for the second element -->
    </temperature>  <!-- end of the second element -->
  </temperature>  <!-- end of the list -->
</Product>

7. Pro-tips

a. nullable fields

All our fields are nullable, which means that, on top of their type, null is also a valid value. By setting a value to null you will delete any previously existing value in that field.

{
  "simpleBoolField": null,
  "localizedText1": null,
  "localizedText2": [{
    "data": null,
    "expressedIn": {
      "code": "fra-FR"
    }
  }]
}


a. Integers (positive integer, negative integer, integer)

"simpleIntegerField1": 1234, OK
"simpleIntegerField2": "", NOK
"simpleIntegerField3": null, OK if field is nullable

b. Floats (positive float, negative float, float)

"simpleFloatField1": 1234, OK
"simpleFloatField2": -56.78, OK
"simpleIntegerField3": "", NOK
"simpleIntegerField4": null, OK if field is nullable

c. Booleans (boolean)

"booleanField1": true, OK
"booleanField2": false, OK
"booleanField3": "", NOK
"booleanField4": null, OK if field is nullable

d. Dates (date)

"dateField1": 1536710400, OK
"dateField2": "", NOK
"dateField3": null, OK if field is nullable

e. Strings (string, text, gln)

"simpleString1": "I am a small text", OK
"simpleString2": "", OK
"simpleString3": null, OK if field is nullable

f. String lists (string_list)

"stringListField1": ["string1", "", null]  OK
"stringListField2": null, OK if field is nullable
"stringListField3": []  OK


g. Entities (code list, kind, brand)

"colorEntityField1": {"code1": "WHITE"}  NOK --> it's {"code": "WHITE"}, in which case it's OK if the code exists for this field
"colorEntityField2": null OK if field is nullable


h. Products (product)

"productField1": {"gtin": "01234567890005"}, OK
"productField2": {"gtin": ""}, NOK
"productField3": {"gtin": null}, NOK
"productField4": null OK

i. String declinable by lang

"localizedText1": [{"data": "My text in english", "expressedIn": { "code": "eng-GB"}}] OK
"localizedText2": [{"data": "", "expressedIn": null}]  NOK
"localizedText3": [{"data": null, "expressedIn": null}]  NOK

j.  Float declinable by unit of measure

{
  "netWeight": [
    {
      "data": 17.3,
      "expressedIn": { "code": "kg"} OK
    },
    {
      "data": 38.14,
      "expressedIn": null, NOK
    },
    {
      "data": null,
      "expressedIn": {"code": "g"}, NOK
    },
    {
      "data": "",
      "expressedIn": {"code": "lb"}, NOK
    },
    {
      "data": "",
      "expressedIn": null, NOK
    },
    {
      "data": null,
      "expressedIn": null, NOK
    }
  ]
}

 

k. Dictionaries (dict)

"batteries1": {
  "areBatteriesIncluded": false,
  "areBatteriesRequired": true
}, OK

"batteries2": {
  "areBatteriesIncluded": null,
  "areBatteriesRequired": null
}, OK if subfield is nullable

"batteries3": null OK if field is nullable
 

l. Lists (list)

"temperature": [
  {"min1": [
    {"data": 7, "expressedIn": {"code": "°C"}},
    {"data": 44.6, "expressedIn": {"code": "°F"}}
  ], OK
  {"min2": [
   {"data": null, "expressedIn": null,
   {"data": "", "expressedIn": {"code": "°F"}}
  ], NOK
  "max1": [], OK
  "max2": null, OK
  }
]
<Product>
  <simpleBoolField />
  <localizedText1 />
  <localizedText2 languages="fra-FR" />
</Product>

b. finding a text in a specific language

A common task when reading our API is to select a specific language for a field which contains some localized text. The first snippet will be about selecting the value for a specific locale, and the second one will add a fallback on any value which would be in the same language. For this second task, our referential "languages" exposes a specific attribute called "normalizedCode" on its entities, which holds the ISO2 language code.

# selecting the value for a specific locale
# will raise if no data is found
def get_value_for_locale(data, field_name, locale):
  for node in data.get(field_name) or []:
    if node['expressedIn']['code'] == locale:
      return node['data']
  raise Exception('No data found for locale %r' % locale)
  
# same thing, with fallback on first value with in the given language
def get_value_for_locale_or_lang(data, field_name, locale, lang):
  try:
    return get_value_for_locale(data, field_name, locale)
  except Exception:
    for node in data.get(field_name) or []:
      if node['expressedIn']['normalizedCode'] == lang:
        return node['data']
  raise Exception('No data found for locale %r and lang %r' % (locale, lang))
  
 alk_data = {
  'normalizedText': [{
    'data': 'Text in fra-FR',
    'expressedIn': {
      'code': 'fra-FR',
      'normalizedCode': 'fr',
    },
  }, {
    'data': 'Text in eng-US',
    'expressedIn': {
      'code': 'eng-US',
      'normalizedCode': 'en',
    },
  }, {
    'data': 'Text in eng-GB',
    'expressedIn': {
      'code': 'eng-GB',
      'normalizedCode': 'en',
    },
  }],
}

get_value_for_locale(alk_data, 'normalizedText', 'fra-FR')
# returns 'Text in fra-FR'
get_value_for_locale(alk_data, 'normalizedText', 'eng-GB')
# returns 'Text in eng-GB'
get_value_for_locale(alk_data, 'normalizedText', 'eng-AU')
# raises Exception: No data found for locale 'eng-AU'
get_value_for_locale_or_lang(alk_data, 'normalizedText', 'eng-GB', 'en')
# returns 'Text in eng-GB'
get_value_for_locale_or_lang(alk_data, 'normalizedText', 'eng-AU', 'en')
# returns 'Text in eng-US'
get_value_for_locale_or_lang(alk_data, 'normalizedText', 'spa-ES', 'es')
# raises Exception: No data found for locale 'spa-ES' and lang 'es'

c. Retrieve the quantity of nutrients for a given quantity

def get_declinable_per_unit(lst, unit):
    for elem in lst:
        if elem['expressedIn']['code'] == unit:
            return elem
    return None


def get_quantity_of_nutrient_per_quantity(product, quantity, unity, nutrient_code):
    for partition in product.get('isPartitionedBy', []):
        elem = get_declinable_per_unit(partition['quantity'], unity)
        if elem is None:
            # No quantity in gram in this partition
            continue
        if elem['data'] != quantity:
            continue
        for nutrient in partition['contains']:
            if nutrient['nutrientCode']['code'] != nutrient_code:
                continue
            if len(nutrient['quantity']) == 0:
                raise Exception("No quantity specified for nutrient: %s" % nutrient_code)
            return '%s%s of product contains %s%s of %s' % (
                quantity,
                unity,
                nutrient['quantity'][0]['data'],
                nutrient['quantity'][0]['expressedIn']['code'],
                nutrient_code,
            )
    return '%s%s contains no measured %s' % (
        quantity,
        unity,
        nutrient_code,
    )


alk_data = {
    "gtin": "03663215001100",
    "isPartitionedBy": [
        {
            "contains": [
                {
                    "nutrientCode": {
                        "code": "food_energy_kJ",
                        "description": "Energy value (kJ)",
                        "label": "energy value (kJ)"
                    },
                    "percentageOfDailyValueIntake": 19.3,
                    "quantity": [
                        {
                            "data": 1621,
                            "expressedIn": {
                                "code": "kJ",
                                "description": "kilojoule",
                                "label": "kilojoule"
                            }
                        }
                    ],
                    "quantityMeasurementPrecision": {
                        "code": "~",
                        "description": "Approximately",
                        "label": "approximately"
                    }
                },
                {
                    "nutrientCode": {
                        "code": "sugars",
                        "description": "Sugars",
                        "label": "sugars"
                    },
                    "percentageOfDailyValueIntake": 1.67,
                    "quantity": [
                        {
                            "data": 1.5,
                            "expressedIn": {
                                "code": "g",
                                "description": "Gram (unit of mass)",
                                "label": "gram"
                            }
                        }
                    ],
                    "quantityMeasurementPrecision": {
                        "code": "~",
                        "description": "Approximately",
                        "label": "approximately"
                    }
                }
            ],
            "name": [
                {
                    "data": "For 100gr",
                    "expressedIn": {
                        "code": "fra-FR",
                        "description": "French",
                        "label": "French",
                        "normalizedCode": "fr"
                    }
                }
            ],
            "preparationState": {
                "code": "UNPREPARED",
                "description": "If the nutrients supplied correspond to the nutrition values of the food in the state in which it is sold",
                "label": "unprepared"
            },
            "quantity": [
                {
                    "data": 100,
                    "expressedIn": {
                        "code": "g",
                        "description": "Gram (unit of mass)",
                        "label": "gram"
                    }
                }
            ]
        },
        {
            "contains": [
                {
                    "nutrientCode": {
                        "code": "proteins",
                        "description": "Proteins",
                        "label": "proteins"
                    },
                    "percentageDVIMeasurementPrecision": None,
                    "percentageOfDailyValueIntake": 7.3,
                    "quantity": [
                        {
                            "data": 3.65,
                            "expressedIn": {
                                "code": "g",
                                "description": "Gram (unit of mass)",
                                "label": "gram"
                            }
                        }
                    ],
                    "quantityMeasurementPrecision": {
                        "code": "=",
                        "description": "Exactly",
                        "label": "exactly"
                    }
                },
                {
                    "nutrientCode": {
                        "code": "salt",
                        "description": "Salt",
                        "label": "salt"
                    },
                    "percentageDVIMeasurementPrecision": None,
                    "percentageOfDailyValueIntake": 0.08,
                    "quantity": [
                        {
                            "data": 0.005,
                            "expressedIn": {
                                "code": "g",
                                "description": "Gram (unit of mass)",
                                "label": "gram"
                            }
                        }
                    ],
                    "quantityMeasurementPrecision": {
                        "code": "=",
                        "description": "Exactly",
                        "label": "exactly"
                    }
                }
            ],
            "name": [
                {
                    "data": "50g",
                    "expressedIn": {
                        "code": "fra-FR",
                        "description": "French",
                        "label": "French",
                        "normalizedCode": "fr"
                    }
                }
            ],
            "preparationState": {
                "code": "UNPREPARED",
                "description": "If the nutrients supplied correspond to the nutrition values of the food in the state in which it is sold",
                "label": "unprepared"
            },
            "quantity": [
                {
                    "data": 50,
                    "expressedIn": {
                        "code": "g",
                        "description": "Gram (unit of mass)",
                        "label": "gram"
                    }
                }
            ]
        }
    ]
}

print '# %s nutrient info:' % alk_data['gtin']
print '\t- %s' % get_quantity_of_nutrient_per_quantity(alk_data, 100, 'g', 'sugars')
print '\t- %s' % get_quantity_of_nutrient_per_quantity(alk_data, 50, 'g', 'proteins')
print '\t- %s' % get_quantity_of_nutrient_per_quantity(alk_data, 11, 'g', 'salt')

# # 03663215001100 nutrient info:
#   - 100g of product contain 1.5g of sugars
#   - 50g of product contain 3.65g of proteins
#   - 11g of product contain no measured salt