How to reuse data type by using API Fragments

Last Updated on 04/05/2021 by Patryk Bandurski

Raml 1.0 introduces a new concept called DataType.  This fragment is used to declare type in a separate YAML file. This is not just a simplification of JSON schema to conform to RAML/YAML. It also brings simple improvements and syntax sugar that allows writing types in a more concise form. And therefore more readable. In this article, we will look at how to define our own data types and reuse them in API and its implementation.

Custom data types

Anyone who has designed an API knows that it is probably always necessary to define custom data structures as simple data types are not meaningful enough. Below you can see data types that are supported by Raml 1.0. We have a group of scalar types like integer, date-only, and so on. We have also compound structures like arrays and objects. XSD and JSON schema is a separate type as developers can still share custom types using these schemas.

Data types in Raml 1.0 (source: github.com/raml-org/raml-spec/)
Data types in Raml 1.0 (source: github.com/raml-org/raml-spec/)

First, we will prepare a sample JSON schema and XSD. Then I will show you the processes of moving them to DataType fragments. Finally, we will see how they are used in API and the Mule project.

JSON Schema

In our service I would like to operate on case resource. Here is the sample object:

{
  "ID": "ABC",
  "Date": "2019-01-01",
  "Description": "I would like ... ",
  "Subject": "Query",
  "Priority": 1,
  "Type": "03"
}

Below I have attached JSON schema for this entity:

{
	"$schema": "http://json-schema.org/draft-04/schema#",
	"description": "Custom Schema",
	"definitions": {
		"date-only": {
			"type": "string",
			"pattern": "^((19|20)\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$"
		},
		"case-type": {
			"type": "string",
			"enum": [
				"01",
				"02",
				"03"
			]
		},
		"case": {
			"description": "Case schema",
			"type": "object",
			"properties": {
				"ID": {
					"type": "string"
				},
				"Date": {
					"$ref": "#/definitions/date-only"
				},
				"Type": {
					"$ref": "#/definitions/case-type"
				},
				"Description": {
					"type": "string"
				},
				"Subject": {
					"type": "string"
				},
				"Priority": {
					"type": "number"
				}
			}
		},
		"cases": {
			"type": "array",
			"items": {
				"$ref": "#/definitions/cases"
			}
		}
	}
}

I have prepared four definitions. date-only and case-type are reusable types that can be used anywhere in the schema file. The main case entity uses both date-only and case-type. For a GET /cases operation, we need an array of cases. That is the reason why the cases type has been prepared.

XML Schema

Previously described object can be represented in XML format. Here is the example case object:

<Case>
	<ID>ABC</ID>
	<Date>2019-01-01</Date>
	<Description>I would like ...</Description>
	<Subject>Query</Subject>
	<Priority>1</Priority>
	<Type>03</Type>
</Case>

And here is XSD:

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://profit-online.pl/blog" targetNamespace="http://profit-online.pl/blog" version="1.0" elementFormDefault="qualified" attributeFormDefault="unqualified">
	<simpleType name="CaseType">
		<restriction base="string">
			<enumeration value="01"/>
			<enumeration value="02"/>
			<enumeration value="03"/>
		</restriction>
	</simpleType>
	<complexType name="Case">
		<sequence>
			<element name="ID" type="string" minOccurs="0"/>
			<element name="Type" type="tns:CaseType" minOccurs="0"/>
			<element name="Description" type="string" minOccurs="0"/>
			<element name="Date" type="date" minOccurs="0"/>
			<element name="Subject" type="string" minOccurs="0"/>
			<element name="Priority" type="number" minOccurs="0"/>
		</sequence>
	</complexType>
	<complexType name="Cases">
		<sequence>
			<element name="Case" type="tns:Case" minOccurs="0" maxOccurs="unbounded"/>
		</sequence>
	</complexType>
</schema>

Raml DataType fragment

Custom DataType can be used within your API definition or can be shared by using API fragments. The result is the same just the opportunity to reuse the model is emphasized by the latter solution.

In order to create a new API fragment in the Design Center click + Create new button and then New Fragment. In the next pop-up window, you need to provide Fragment Name and Fragment Type. For my case demo, it is Case and Data Type.

So how can I model previously shown JSON and XML representation of the Case? As DataType can be serialized to both XML and JSON we do not need two separate models. However, we may enrich it with tips for XML serializer. For example, we may declare some property as an attribute.

#%RAML 1.0 DataType

type: object
description: Case schema

properties:
  ID?:
  Date:
    type: date-only
    required: false
  Type:
    enum: [ "01", "02", "03"]
    required: false
  Description?:
  Subject?:
  Priority:
    type: number
    required: false

xml:
  namespace: https://www.ambassadorpatryk.com/blog

example:
  ID: ABC
  Date: 2021-05-03
  Description: I would like ...
  Subject: Query
  Priority: 1
  Type: "03"

We know that we need to declare an object (line 3). Then we define all available properties for it. In the 6th line, I have declared an optional ID field and is of type string. This is a handy shortcut in comparison to a full declaration like in lines 7-9. The reason why I did not use a question mark for the Date field is its data type.  We needed to declare it, and it is different than string.

For XML I would like to have a custom namespace that can be declared in xml.namespace property. Apart from that, we may define example(s) either using JSON/XML raw notation or using YAML notation like in the example above. Example in YAML is converted to JSON in API Console and Anypoint Exchange.

If you compare this DataType declaration to JSON schema you will definitely see similarities. So if you are fluent in writing JSON Schema you should be able to write as well as easily custom Data Types. More about DataTypes you can find in Raml 1.0 Specification.

After the model has been prepared we need to publish it into an Exchange in order to use this in API specification. So how to use the shared model?

Usage

First, we need to create an API specification.  We can do this in Design Center. In my demo, I have named my API Spec as Case SAPI.  It is a simple CRUD service. Here is the API without model attached.

#%RAML 1.0
title: Case SAPI
mediaType: 
  - application/json
  - application/xml

/cases:
  get:
  post:
  
  /{id}:
    put:
    get:
    delete:

As you can see we allow to perform GET and  POST methods on cases resource and GET, DELETE and PUT on {id} resource. As you may expect service will return arrays as well as single items.  In the 3rd line, I have declared that API supports two media types both JSON and XML.

This is fairly simple API. Now we need to attach data model.

Using DataType from API Fragment

If we have Data Types exposed as API fragments on the Exchange we need to import them as dependencies. In order to do this, we need to click the Exchange dependencies button on the left-hand side ( graph icon).  In the Dependencies window, we have all API fragments that we have already imported and we can add more. In the Exchange Dependencies window, we can see all ours fragments and already prepared by MuleSoft. I have decided to import Case fragment. I also added the gif down below with adding the dependency.

Import API Fragment dependency from Anypoint Exchange into API Spec
Import API Fragment dependency from Anypoint Exchange into API Spec

Now in the Dependencies view, I can see one item: Case 1.0.0.  This implies that I have imported the Case API Fragment in version 1.0.0. Here I can change the version of this API, see all files or remove dependency completely.  Behind the scene, files from Case are copied to the exchange_modules folder. In the screenshot below you can see the sample structure. Folder exchange_modules is accessible only in read-only mode.

Imported dependencies stored in exchange_modules read-only folder
Imported dependencies stored in exchange_modules folder

Below you can see updated API. In 7th line we include previously defined case-type in DataType file. Next we refer to this type as a Case (line 6). Let’s see how do we enable our service to work with both XML and JSON content just based on single data type definition.

#%RAML 1.0
title: Case SAPI
mediaType: 
  - application/json
  - application/xml

types:
  Case: 
    type: !include /exchange_modules/073d1428-0a98-4fc0-b0b6-64c37cfac800/case/1.0.0/case.raml

/cases:
  get:
    responses:
      200:
        body:
          application/json:
            type: Case[]
          application/xml:
            type: Case[]
            xml:
              wrapped: true
  post:
    body: Case
    responses:
      201:
  
  /{id}:
    put:
      body: Case
      responses:
        204:
    get:
      responses:
        200:
          body: Case
    delete:
      responses:
        204:

Let’s start with the simple POST case operation. In line 23 we set the body to Case. This is a direct simple assignment. We may define type property as a child and there assign Case type. This is a longer option however it gives more flexibility to configure additional properties.  As you noticed, we may use this kind of assignment for both media types. The conversation to desired output is done under the hood.

XML root element

Below you have two lines of a long XML document. Is it a valid XML block?

<?xml version="1.0" encoding="UTF-8"?>
<user name="Harry" lastName="Potter" />
<user name="Albus" lastName="Dumbledore" />

The answer is it is not. XML document must have only one root element.  In the depicted example, we have two roots called user. We may fix this by wrapping it. To wrap it we may use a pluralized version of the already existing elements. In other words, we may wrap it in users.

For XML media type Case will be used as a wrapper for all elements. In lines 16 – 21 we have defined response body by media type. You may ask why I cannot do it once like before. Because you need to define a wrapper for XML format (lines 20 and 21). Without this, we would return array items without any wrapper around them. And now you know that this is considered as a not valid XML.

Let’s go to the implementation now.

Implementation

During creating the project you can specify the API definition. You can point to a local RAML file or published API on Anypoint Exchange.  The latter option is of course the recommended one. I use it for all APIs I work on. After your project is created and the API scaffolded, you should make some cleaning in the folder. It is a good practice to put all global configurations into a single file called global.xml. You may remove API Console if you do not need it.

POST cases

When you decide to expose response in more than one media type you need to declare a separate private flow for each. In my case, I have two flows for creating a Case like below

  • post:\cases:application\json:case-sapi-config
  • post:\cases:application\xml:case-sapi-config

In this case, you should create another flow encapsulating operation logic apart from transformation. It can be done by calling private flows. As my demo case is really simple I have only transformations. For json it is straightforward as depicted below:

%dw 2.0
output application/json
---
{
	ID: payload.ID
}

For XML output it is a littile bit more complicated but still rather straightforward:

%dw 2.0
output application/xml
ns ns0 https://www.ambassadorpatryk.com/blog
---
{
	ns0#Case: {
		ns0#ID: payload.ns0#Case.ns0#ID
	}
}

The major difference is the usage of the namespace. I have declared namespace called ns0 to value defined in my DataType (xml.namespace). Worth mentioning is the wrapper. As you remember we need a root element (here Case).

PUT and other methods with body behave simillar, so I skip them. How about GETs?

GET cases

For the GET method, we can define different media types only for the response. We do not send a content-type header for the GET calls. That is why we have only one flow. In one of my previous blog posts I described that in that situation you can distinguish type by Accept header sent by the client. That is the reason why we have a choice component in the middle of the flow. It checks if header Accept has value application/xml or not. If not by default JSON output is generated.

Validation

API Kit router validates incoming messages against our DataType. It knows that we allowed both XML and JSON requests and it uses our custom type for validating both. I would like to be sure if everything that I am sending back to the client is as well 100% valid against my schema. Unfortunately API Kit router does not do it.  You may ask why should we validate the outgoing messages. I would say, to be perfectly sure that your service won’t surprise your clients and you. You may make an assumption that everything will work fine but that might be not true.  From my real-life experience, I know that sometimes services returns responses with mistakes. It may be a human mistake but still, it can occur. In this case, we could use the JSON or XML Module. Unfortunately, it does not allow the DataType definition.

Summary

DataType is a really good way to define your custom type in a concise way. Semantic is similar to JSON Schema but with improvements. It is very impressive that I can use one DataType with different media types. It saves time and mitigates the risk that your schemas won’t match. However, we can’t reuse it in XML or JSON Modules as it only accepts JSON Schema and XSD.

How to reuse data type by using API Fragments

One thought on “How to reuse data type by using API Fragments

  1. Thank you very much for this article. It explains a lot.
    Dziekuje i pozdrawiam.

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top