Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Reuse Driven by DataType

DZone's Guide to

Reuse Driven by DataType

In this article, we will look at how to define our own data types and reuse them in the API and its implementation.

· Integration Zone ·
Free Resource

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 raml/yaml. It also brings simple improvements and syntax sugar that allows to write types in 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 the 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 array and objects. XSD and JSON schema are a separate type as a developer can still share custom types using these schemas.

Image title


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 an API and 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. Main case entity uses both date-only and case-type. For get /cases operation, we need an array of cases. That is the reason why cases type has been prepared.

XML Schema

The 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"></element>
    </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 model is emphasized by the latter solution.

In order to create a new API fragment in the Design Center, click the + Create button and then API fragment. In the next popup windows, you need to provide Project Name and Fragment Type. For my case demo, it is Case Model and Data Type.

So, how can I model previously shown JSON and XML representation of Case(s)? 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.

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 ID field that is optional and is of type string. This is a handy shortcut in comparison to the full declaration like in lines 7-9. The reason why we did not use a question mark for 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. The 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 also be able to easily write custom Data Types. You can find more about DataTypes 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 do we use the shared model?

Usage

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

Image title

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, the service will return arrays as well as single items. In the 3rd line, I have declared that the API will allow two media types: both JSON and XML.

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

Applying Model to API

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 the API fragments that we have already imported, and we can add more. In the Consume API Fragment window, shown below, we can see all our fragments and already prepared by MuleSoft. In my case, I have decided to import the Case Model.

Image title

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

Image title

Below, you can see the updated API. In the 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 we enable our service to work with both XML and JSON content just based on a single data type definition.

Let's start with the simple POST case operation. In line 21, we set 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 a result, we may use this for both media types.

Root Element

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

<invoke-static doc:name="Generate Gender" class="com.profitonline.generators.GenderGenerator" method="generate()"/>
<invoke-static doc:name="Generate Number" class="com.profitonline.generators.NumberGenerator" method="generate()">

If you answered NO, then you are right.

XML document must have only one root element. In the depicted example, we have two roots called invoke-static. 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 invoke-statics.

For XML, media type Case will be used as a wrapper for all elements. In lines 14-19, we have defined the 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 (line 18 and 19). Without this, we would return array items without any wrapper around it, and now you now that this is considered a not valid XML.

Now we are to implement it.

Implementation

During the creation of the project, you can specify the API definition file. You can point to a local file or directly from the Design Center. After that, you should do 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. I also split operations into different files. In the diagram file, you can see a demo configuration files. In the resources folder, you can find your imported API and dependencies from exchange. As you can see, files are directly copied into a directory.

Image title

POST Cases

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

  • post:\cases:application\json:case-api-config
  • post:\cases:application\xml:case-api-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 little bit more complicated but still rather straightforward:

%dw 2.0
output application/xml
ns ns0 http://www.profit-online.pl/blog
---
{
  ns0#Case: {
    ns0#ID: payload.ns0#Case.ns0#ID
  }
}

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

PUT and other methods with body would behave similarly. How about GETs?

GET Cases

For GET, we can define different media types only for response (not only, but it is recommended). We do not send content-type header for GET calls. That is why we have only one flow. In one of my previous articles, 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 100% valid against my schema. Unfortunately, API Kit router does not do it. You may ask why should we validate outgoing messages. I would say to be perfectly sure that your service does not surprise your clients or you. You may make an assumption that everything will work fine, but that might not be true. From my experience, I know that sometimes services return responses with mistakes. It may be a human mistake, but it can still occur. In this case, we could use the Validation Message Processor, however, it does not allow DataType definition.

Unique Particle Attribution

When I sent XML request to my application, I received a peculiar error message about Unqiue Particile Attribution:

Error validating XML. Error: cos-nonambig: "http://www.profit-online.pl/blog":ID and WC[##any] (or elements from their substitution group) violate "Unique Particle Attribution". During validation against this schema, ambiguity would be created for those two particles.

I suspected that I have some errors in my DataType, and I was right. It turned out that by mistake, I wrote something like:

properties:
  ID?:
  Date?:
    type: date-only

You may see what is wrong here. In the 3rd line, I have made a mistake. We are not supposed to define further Date property if we use a question mark.

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 Validation Message Processor as it only accepts JSON Schema and XSL.

Sync, automate, and notify lead to customer changes across marketing, CRM, and messaging apps in real-time with the Cloud Elements eventing framework. Learn more.

Topics:
integration ,datatypes ,json ,xml ,schema ,api

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}