{"id":4740,"date":"2022-11-28T18:43:43","date_gmt":"2022-11-28T18:43:43","guid":{"rendered":"https:\/\/dalelane.co.uk\/blog\/?p=4740"},"modified":"2022-12-01T19:14:58","modified_gmt":"2022-12-01T19:14:58","slug":"using-ibm-datastage-to-process-json-events-on-apache-kafka-topics","status":"publish","type":"post","link":"https:\/\/dalelane.co.uk\/blog\/?p=4740","title":{"rendered":"Using IBM DataStage to process JSON events on Apache Kafka topics"},"content":{"rendered":"<p><strong>In this post, I share a step-by-step guide for how to use IBM DataStage to merge JSON messages from multiple different Apache Kafka topics, into a single joined-up stream of events.<\/strong><\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/hib78lotup4yabf\/66-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" style=\"border: thin black solid\" src=\"https:\/\/www.dropbox.com\/s\/3xct7tfvv81buzr\/66-flow.png?raw=1\"\/><\/a><\/p>\n<p><!--more-->I needed to use IBM DataStage for the first time last week. As this was new to me, it feels interesting enough to share. (If nothing else, putting my notes here will probably be useful to me when I next need to remind myself how I got started!)<\/p>\n<p><em>Warning: This is the first DataStage flow I&#8217;ve created, so I&#8217;m not an authority and can&#8217;t claim this is best practice. It seems to work as far as I can tell, but it&#8217;s entirely possible I&#8217;m doing something daft (so please let me know if you see any mistakes!)<\/em><\/p>\n<p>For my first flow, I wanted to try merging multiple streams of events into a single joined up stream. To keep the walkthrough in this post easier to follow, I&#8217;ve kept it to just two topics, but I used this approach to combine several topics in the same way.<\/p>\n<hr \/>\n<p>My starting point was to create a Kafka cluster using <a href=\"https:\/\/ibm.github.io\/event-streams\">IBM Event Streams<\/a>, from <a href=\"https:\/\/www.ibm.com\/products\/cloud-pak-for-integration\">Cloud Pak for Integration<\/a>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/yovxu9yz5cls2g9\/01-event-streams.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/1chqzfvvac9g53u\/01-event-streams.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I have a collection of topics for stock price updates. Each topic receives events every minute for a different company.<\/p>\n<p>(<em>If you&#8217;d like to know how I get a <a href=\"https:\/\/dalelane.co.uk\/blog\/?p=4463\">Kafka topic with stock price events<\/a>, I&#8217;ve written about that before, but the short version is I&#8217;m using a <a href=\"http:\/\/github.com\/dalelane\/kafka-connect-stockprice-source\">custom Kafka Connect connector<\/a>.<\/em>)<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/qcsyh4ud4ynsbh6\/02-event-streams-topic-apple.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/buqadzenpu1pl51\/02-event-streams-topic-apple.png?raw=1\"\/><\/a><br \/>\n<a href=\"https:\/\/www.dropbox.com\/s\/l8vg4ujz6npmfzy\/03-event-streams-topic-microsoft.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ly5df3cpohnctvq\/03-event-streams-topic-microsoft.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Now I was ready to start processing my Kafka messages, using <a href=\"https:\/\/www.ibm.com\/uk-en\/products\/datastage\">IBM DataStage<\/a> from <a href=\"https:\/\/www.ibm.com\/products\/cloud-pak-for-data\">Cloud Pak for Data<\/a>.<\/p>\n<p>First up, I clicked on <strong>New project<\/strong> to create my project.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/2n262u44dsks5of\/04-cp4d.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/5uywf4eqnsk48wc\/04-cp4d.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I went with an <strong>empty project<\/strong> for this.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/kjv7787r8ql38mi\/05-new-cp4d-project.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/eq1rnsa71k93yyd\/05-new-cp4d-project.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>To start with, there are a few assets I wanted to get ready. So I clicked on to the <strong>Assets<\/strong> tab then clicked the <strong>New asset<\/strong> button.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/uwh5ofeqx84exev\/06-project-assets.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/5pjfzf1po483rbq\/06-project-assets.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The first thing to get ready was the connection to the Kafka cluster, so I chose the <strong>Connection<\/strong> asset type.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/h4axh0plmghftz5\/07-new-asset-connection.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/pjsgb90gnsm8847\/07-new-asset-connection.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Lots of connection types to choose from &#8211; I wanted <strong>Apache Kafka<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zc8n322kacu79ic\/08-new-connection-kafka.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/t1uwnim5v0wpvtp\/08-new-connection-kafka.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I called my connection &#8220;Event Streams&#8221; because I lack imagination.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/c8436s8zdlnwm04\/09-new-connection-name.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/qmgpt7cy6wc6hof\/09-new-connection-name.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Next thing I needed was the bootstrap address for the Kafka cluster, which I got by clicking on the copy button for the External listener.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zhrt28ia6e2chv9\/10-event-streams-bootstrap.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/1dnrvt5zpblt5qx\/10-event-streams-bootstrap.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I pasted that into the <strong>Kafka server host name<\/strong> field.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/l963zog1tnen12l\/11-connection-bootstrap.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/oy97nvdwa83rt4c\/11-connection-bootstrap.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I needed some credentials for DataStage to use, so I clicked the <strong>Generate SCRAM credentials<\/strong> button, and used the wizard to create a new username and password.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/bbfjc39dqf9f4sc\/12-event-streams-creds.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/bl3imcfx0bb84fj\/12-event-streams-creds.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I pasted those into the credentials section of the DataStage connection form.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/n88zke693ax8ai0\/13-connection-creds.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/mqqx7yzgwlczrz7\/13-connection-creds.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Finally, I needed to download the truststore certificate for the Kafka listener, which I got by clicking the download button for the <strong>PEM certificate<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/nedxn8vc1mbwiqo\/14-event-streams-pem.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/wjfo08d5bioupex\/14-event-streams-pem.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I pasted the contents of that pem file into the <strong>Truststore certificates<\/strong> box on the DataStage connection form.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/gfdxohwt5ix6ugu\/15-connection-pem.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/judpv8aweoljtc7\/15-connection-pem.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>First asset ready, two more to go.<\/p>\n<p>Next was storing a schema that describes the JSON messages that we&#8217;ll be consuming from the Kafka topics.<\/p>\n<p>Back to the <strong>New asset<\/strong> button to start that.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/qyf8w108izeai2u\/16-first-asset.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/9qmv4yjiykwpefv\/16-first-asset.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Schema libraries are included in the <strong>DataStage component<\/strong> section.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/izjwz4qfcr59elw\/17-datastage-component.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/w8lu998wwg68b1u\/17-datastage-component.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I chose the <strong>Schema library<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/okgagcbdli06pz6\/18-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/5zjke8e88np17y5\/18-schema-library.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>This library was for the definitions of input messages &#8211; the messages on the topics like <code class=\"mqunicode\">STOCK.PRICES.APPLE<\/code> and <code class=\"mqunicode\">STOCK.PRICES.MICROSOFT<\/code> that I wanted to consume.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/scbwwqcv1y3af3k\/19-new-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/afsd2mcqnmex5eh\/19-new-schema-library.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I clicked on <strong>Add resource<\/strong> to upload my JSON file.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/343b2oxmdgzcje5\/20-empty-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ocga1jbcuul28n1\/20-empty-schema-library.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>You don&#8217;t have to upload a schema &#8211; you can just upload an example message, and DataStage will generate a schema from that.<\/p>\n<p>So I went back to the Event Streams web interface, and clicked on the <strong>Copy<\/strong> to copy the contents of the most recent message.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/qcsyh4ud4ynsbh6\/02-event-streams-topic-apple.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/buqadzenpu1pl51\/02-event-streams-topic-apple.png?raw=1\"\/><\/a><\/p>\n<p>My <code class=\"mqunicode\">sample.json<\/code> file looked like:<\/p>\n<pre class=\"mqunilisting\">{\n    \"open\": 147.4,\n    \"high\": 147.58,\n    \"low\": 147.4,\n    \"close\": 147.57,\n    \"volume\": 56044,\n    \"timestamp\": 1668805140,\n    \"datetime\": \"2022-11-18 15:59:00\"\n}\n<\/pre>\n<hr \/>\n<p>DataStage used my <code class=\"mqunicode\">sample.json<\/code> file to generate a <strong>sample.json.xsd<\/strong> schema.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/a89ipsjbkgihmmh\/21-generate-schema.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/iw5y3im33n8h0r1\/21-generate-schema.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I also wanted to create a schema library for the messages I wanted my DataStage flows to output, so back to the Assets tab to click on <strong>New asset<\/strong> again.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/ctujhn0ly7lqeas\/22-second-asset.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/yzagopl4ckcbkb4\/22-second-asset.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>As before, I clicked into <strong>DataStage component<\/strong> and then <strong>Schema library<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/bxg12a4w5nz8bfo\/23-datastage-component.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/vdcec0kzu1bcbwp\/23-datastage-component.png?raw=1\"\/><\/a><br \/>\n<a href=\"https:\/\/www.dropbox.com\/s\/i6x2xezdavuo59u\/24-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/mk6hqhhdao83jf3\/24-schema-library.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I named this schema library to show I&#8217;d use it to store the schema of output messages.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/pfecd9ynhnmya5h\/25-new-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/63ogs2bwybgcm6s\/25-new-schema-library.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Then clicked on <strong>Add resource<\/strong> to upload my JSON file.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/hcf6lzrqtg9dv00\/26-empty-schema-library.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/6z4e83poqad1vip\/26-empty-schema-library.png?raw=1\"\/><\/a><\/p>\n<p>As before, I didn&#8217;t upload a schema, I uploaded a file with a sample message payload and let DataStage generate a schema for me.<\/p>\n<p>I created a file called <code class=\"mqunicode\">combined.json<\/code> and came up with this idea for a simple combined payload.<\/p>\n<pre class=\"mqunilisting\">{\n    \"timestamp\": 1668805140,\n    \"datetime\": \"2022-11-18 15:59:00\",\n    \"apple\": {\n        \"open\": 147.4,\n        \"high\": 147.58,\n        \"low\": 147.4,\n        \"close\": 147.57,\n        \"volume\": 56044\n    },\n    \"microsoft\": {\n        \"open\": 147.4,\n        \"high\": 147.58,\n        \"low\": 147.4,\n        \"close\": 147.57,\n        \"volume\": 56044\n    }\n}\n<\/pre>\n<p>(The numbers aren&#8217;t accurate &#8211; but they didn&#8217;t need to be. I just wanted a sample of the shape of the data that could be used to generate a schema).<\/p>\n<hr \/>\n<p>DataStage used my <code class=\"mqunicode\">combined.json<\/code> file to generate a <strong>combined.json.xsd<\/strong> schema.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/gvjmpjio1ttlm6p\/27-generated-schema.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/pu6ol3hsgfj9qn2\/27-generated-schema.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Preparation done &#8211; now I was ready to create my flow.<\/p>\n<p>Time to click the <strong>New asset<\/strong> button for the last time.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/h0c56rl00as04o1\/28-third-asset.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/n1ofc77txb4c5tn\/28-third-asset.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>This time I chose <strong>DataStage<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zrdaj92q56eqksl\/29-new-asset.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/2ruyo1zp9kc81ti\/29-new-asset.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I gave my flow the name <strong>share-prices-combiner<\/strong>. Still feeling unimaginative.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/wugq26qsl51naof\/30-new-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ee3nd8ouzzmkrp3\/30-new-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Here was my shiny new canvas, ready to start setting up a flow.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/7645bl51lmfsjla\/31-empty-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/4c7j352cyncj768\/31-empty-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Here is my finished flow.<\/p>\n<p>I&#8217;ll go through the critical settings I used for each node below. But first, at a high level, the flow:<\/p>\n<ul>\n<li>consumed string messages from two of my stock price Kafka topics<\/li>\n<li>converted the JSON string messages to structured columnar data<\/li>\n<li>joined the two sets of data using the datetime value in each message<\/li>\n<li>flattened the joined columnar data back into a JSON string<\/li>\n<li>produced the JSON string messages to a new Kafka topic<\/li>\n<\/ul>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/enukqmlg6ak18kj\/32-finished-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/xl1oj43hli4izpy\/32-finished-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Starting from the left, the <strong>Apache Kafka<\/strong> node for consuming messages from the <code class=\"mqunicode\">STOCK.PRICES.APPLE<\/code> topic.<\/p>\n<p>I picked the &#8220;Event Streams&#8221; <strong>Connection<\/strong> that I&#8217;d set up before, added the topic name, entered a consumer group id that Event Streams would accept, and left it using <strong>String<\/strong> for deserializing as my JSON payload is a string.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/cl5cmq0ux80von0\/33-input-topic-props.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ctk3fhhz4ccyy81\/33-input-topic-props.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>As my output from this node is just a single string message payload, I defined a single output column which I called &#8220;INPUT_STRING&#8221; and defined as a <code class=\"mqunicode\">VARCHAR<\/code> with a max length of 300. (The default was 100, but most of my message payloads are a bit longer than that.)<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zixeyv78dlts8vt\/34-input-topic-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ycnfgn4yaw74ngt\/34-input-topic-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I did more or less the same for the <strong>Apache Kafka<\/strong> node for consuming messages from the <code class=\"mqunicode\">STOCK.PRICES.MICROSOFT<\/code> topic.<\/p>\n<p>I picked the &#8220;Event Streams&#8221; connection, entered a topic name and consumer group id, and left the rest as defaults.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/hh05gi250fib7g8\/35-input-topic-props.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/6091oosa74s4zjy\/35-input-topic-props.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>And again, I defined a single output column to contain the message payload, calling it &#8220;INPUT_STRING&#8221; so it&#8217;s nice and readable.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zxyaco5k4jh68v1\/36-input-topic-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/87muc0z1bd1d64w\/36-input-topic-output.png?raw=1\"\/><\/a><\/p>\n<p>I named the links out of these Kafka consumer nodes &#8220;aapl_string&#8221; and &#8220;msft_string&#8221; &#8211; so it would be clear:<\/p>\n<ul>\n<li>which stock price is coming down that link (aapl for Apple, and msft for Microsoft)<\/li>\n<li>the structure of the data &#8211; a single string column<\/li>\n<\/ul>\n<hr \/>\n<p>Next, I needed to parse the JSON message strings.<\/p>\n<p>The top one parses the messages from the <code class=\"mqunicode\">STOCK.PRICES.APPLE<\/code> topic. I used a <strong>Hierarchical Data<\/strong> node for this.<\/p>\n<p>The input for this node is the single &#8220;INPUT_STRING&#8221; 300-character VARCHAR column coming from the AAPL Kafka consumer node.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/bv8gios8x96p8i3\/37-apple-parser-input.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ot5eazq6h39hzho\/37-apple-parser-input.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The <strong>Hierarchical Data<\/strong> node represents a subflow, which I set up with a single <strong>JSON parser<\/strong> node.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/jtif2p3821kmxm3\/39-apple-parser-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/mhm64q0h3x3h7w5\/39-apple-parser-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>It&#8217;s a little difficult to see the selections here because they get greyed out after you confirm them, but this is how I configured the JSON parser.<\/p>\n<p>The <strong>JSON source<\/strong> input for the JSON parser was the &#8220;INPUT_STRING&#8221; column coming from the &#8220;aapl_string&#8221; link. So this means I gave the JSON parser the string message payload coming from the STOCK.PRICES.APPLE topic. (You can see this in the right column, too).<\/p>\n<p>And the <strong>Document root<\/strong> identifies the schema to use, so I chose the schema from the <strong>kafka messages<\/strong> schema library that I set up earlier.<\/p>\n<p>There are some other settings for how to handle JSON validation errors, but I didn&#8217;t worry about those as I know that all the messages on my topic are valid JSON matching the schema I&#8217;m using.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zugj0c0yshvy6za\/40-apple-parser-config.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/lyfyb2q2feft7yq\/40-apple-parser-config.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>For the output columns from the JSON parser flow, I created a separate column for each value in my JSON message.<\/p>\n<p>Remember, my JSON message string looks like this:<\/p>\n<pre class=\"mqunilisting\">{\n    \"open\": 147.4,\n    \"high\": 147.58,\n    \"low\": 147.4,\n    \"close\": 147.57,\n    \"volume\": 56044,\n    \"timestamp\": 1668805140,\n    \"datetime\": \"2022-11-18 15:59:00\"\n}\n<\/pre>\n<p>I created an output column for each of the values in the JSON data. I added a prefix to the name just to make it obvious to me later on where each value came from &#8211; so as this is parsing the stock price events for the Apple AAPL stock, I added a &#8220;aapl_&#8221; prefix to each of the column names.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/b2o9m52caf5cu5y\/41-apple-parser-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/hyzfpeo2h8yuvle\/41-apple-parser-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Because my JSON structure is flat with no nested objects, the mapping from the JSON parser to the output columns was nice and simple.<\/p>\n<p>The mapping here is from the fields defined in the JSON schema on the left, to my output columns on the right.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/k8naf6t3m0uha5c\/42-apple-parser-mapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/cwz7uqjsg2mht1c\/42-apple-parser-mapping.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Back to the overall <strong>Hierarchical data<\/strong> node, that means the output columns looked like this.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/v7gvxsgxe2upxgz\/38-apple-parser-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/w8wm7dm076zzhaf\/38-apple-parser-output.png?raw=1\"\/><\/a><\/p>\n<p>I named the link out of this node &#8220;aapl_cols&#8221; to make it clear that this link would have the data from STOCK.PRICES.APPLE but structured as columnar data.<\/p>\n<hr \/>\n<p>Then I did pretty much the same for the Microsoft stock price events.<\/p>\n<p>A <strong>Hierarchical data<\/strong> node for parsing the VARCHAR in the single &#8220;INPUT_STRING&#8221; column coming from the <code class=\"mqunicode\">STOCK.PRICES.MICROSOFT<\/code> topic.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/h6fo41kubmvkvxj\/43-msft-parser-input.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/lvyqco0ou8l8khf\/43-msft-parser-input.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The assembly for the Hierarchical data node is just a single <strong>JSON parser<\/strong> node.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/ofcq6cvn5c6iul5\/45-msft-parser-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/24zgdofy4gla41h\/45-msft-parser-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The config for the JSON parser identifies what input it should parse (the contents of the &#8220;INPUT_STRING&#8221; column coming from the &#8220;msft_string&#8221; link), and which schema it should use to do that (the schema from the &#8220;kafka messages&#8221; schema library).<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/ndmg39vlbov2ft9\/46-msft-parser-config.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/a7v7e4g415lo9gm\/46-msft-parser-config.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The output columns I set up are again based on the values in the message payload, prefixed to make it easier to keep track of where the values came from.<\/p>\n<p>So messages that look like this:<\/p>\n<pre class=\"mqunilisting\">{\n    \"open\": 147.4,\n    \"high\": 147.58,\n    \"low\": 147.4,\n    \"close\": 147.57,\n    \"volume\": 56044,\n    \"timestamp\": 1668805140,\n    \"datetime\": \"2022-11-18 15:59:00\"\n}\n<\/pre>\n<p>will be represented as seven columns, with names starting &#8220;msft_&#8221;.<\/p>\n<p>I left the &#8220;datetime&#8221; value named &#8220;datetime&#8221; in both parsers, as that makes the join easier.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/ctwpe1xz9sahgec\/47-msft-parser-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/xpjeiywr52jnnxc\/47-msft-parser-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Again, I needed to map the values identied in the JSON schema (shown on the left) to the output columns I defined (shown on the right).<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/lfs3c6mnxodf1d5\/48-msft-parser-mapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ax1j4nluxub1d4o\/48-msft-parser-mapping.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>That left my <strong>Hierarchical data<\/strong> node with these output columns.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/qk386uhbaz7mnp8\/44-msft-parser-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/soe1udcty1wvaz0\/44-msft-parser-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Now I had structured data flowing from events on both the <code class=\"mqunicode\">STOCK.PRICES.APPLE<\/code> and <code class=\"mqunicode\">STOCK.PRICES.MICROSOFT<\/code> topics. It was time to merge them into a single stream of events.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/my31w7lt6vuexhr\/49-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ybm1m729ej6qpya\/49-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I used the <code class=\"mqunicode\">datetime<\/code> value as the <strong>Join key<\/strong> for this &#8211; as I called the column holding the datetime value in both aapl_cols and msft_cols output &#8220;datetime&#8221;.<\/p>\n<p>I used an Inner join for this because I&#8217;m looking for events where the same timestamp is found in an event on both the Apple and Microsoft updates topic.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/g6vylt3loscj114\/50-join-key.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/zoyj2ehq514vwu1\/50-join-key.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>You can see the <strong>input columns<\/strong> for the <strong>Join<\/strong> node, split into separate lists for each of the two inputs (&#8220;aapl_cols&#8221; and &#8220;msft_cols&#8221;).<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/bon6j6kooqn94wk\/51-join-input-aapl.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ngmzxfhm0028znw\/51-join-input-aapl.png?raw=1\"\/><\/a><br \/>\n<a href=\"https:\/\/www.dropbox.com\/s\/82qwm4iq61y7dmf\/52-join-input-msft.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/2jcy7zlvh9ad4xy\/52-join-input-msft.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>For the output columns, I basically wanted everything. So (as well as the &#8220;datetime&#8221; column I used for the join) I included all of the &#8220;appl_&#8221; columns&#8230;<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/nuev9gozglj5gu3\/53-join-mapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/rbxt2ob9ehk3wux\/53-join-mapping.png?raw=1\"\/><\/a><\/p>\n<p>and all of the &#8220;msft_&#8221; columns.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/92k0h67ua8dt43g\/54-join-mapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/nu8gjddgra1w3ii\/54-join-mapping.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I called this &#8220;all_cols&#8221; to make it clear that this was structured, columnar data that includes columns for both the Apple and Microsoft data.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/yakx0rhpalbjocd\/55-join-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/sfc34ku05tk4yr2\/55-join-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Next I needed to turn this columnar data into a JSON string, so I used another <strong>Hierarchical data<\/strong> node.<\/p>\n<p>The input columns were all of the joined, combined columns that came out of the <strong>Join<\/strong> node.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/5wlwxm4bz2r3s7g\/56-jsonwriter-input.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/nnxdf6yr8e00py9\/56-jsonwriter-input.png?raw=1\"\/><\/a><br \/>\n<a href=\"https:\/\/www.dropbox.com\/s\/5hwg5ouaejat2qr\/57-jsonwriter-input.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/0gjyyriewun08lm\/57-jsonwriter-input.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I kept the assembly for this nice and simple, with just a single <strong>JSON composer<\/strong>.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/zdulo9dgrtvt2en\/60-jsonwriter-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/qbqfxhw2cpzpekl\/60-jsonwriter-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The config for the JSON composer is a little hard to read, as it gets greyed out once you confirm it, but the main setting was specifying the schema to use &#8211; which I chose from the &#8220;output messages&#8221; schema library that I prepared before.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/57vgcsj3h8crwkv\/59-jsonwriter-config.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/en202518krce5iq\/59-jsonwriter-config.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>Remember that the JSON payload I invented for the joined up events looks like this:<\/p>\n<pre class=\"mqunilisting\">{\n    \"timestamp\": 1668805140,\n    \"datetime\": \"2022-11-18 15:59:00\",\n    \"apple\": {\n        \"open\": 147.4,\n        \"high\": 147.58,\n        \"low\": 147.4,\n        \"close\": 147.57,\n        \"volume\": 56044\n    },\n    \"microsoft\": {\n        \"open\": 147.4,\n        \"high\": 147.58,\n        \"low\": 147.4,\n        \"close\": 147.57,\n        \"volume\": 56044\n    }\n}\n<\/pre>\n<p>To configure the <strong>JSON composer<\/strong>, I needed to map each of the &#8220;aapl_&#8221; and &#8220;msft_&#8221; columns to the values in my JSON structure.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/6bv2j7fgirntqxk\/62-jsonwriter-mapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ym1uhthxemp6cua\/62-jsonwriter-mapping.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>As I wanted to give the Kafka producer node a single string to write, I defined a single output column. I used a string serializer for writing to the output topic, so I made the output column a VARCHAR with a generous max length to hold the combined output. I called this column &#8220;OUTPUT_STRING&#8221; to make it obvious what it would contain.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/k7xpllxz9lwqoxt\/61-jsonwriter-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/ypf9yul8c56kzay\/61-jsonwriter-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I configured the overall <strong>Hierarchical data<\/strong> assembly by mapping the string output of the JSON composer to my &#8220;OUTPUT_STRING&#8221; output column.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/pcrneeqh59z2cko\/63-jsonwriter-outputmapping.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/803zp8ajk5ptr36\/63-jsonwriter-outputmapping.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I named the output link for the <strong>Hierarchical data<\/strong> node &#8220;all_str&#8221;, to show that it was the combined stock price events for all shares, in a single string representation.<\/p>\n<p>The output column going down this link is the single &#8220;OUTPUT_STRING&#8221; VARCHAR column.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/g6u2v7gu8cb5v2o\/58-jsonwriter-output.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/zko2duxnc075npr\/58-jsonwriter-output.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The final node in the flow is another <strong>Apache Kafka<\/strong> node, this time being used as a Kafka producer.<\/p>\n<p>I chose the &#8220;Event Streams&#8221; <strong>Connection<\/strong> that I created before, and told it to produce to a new <code class=\"mqunicode\">STOCK.PRICES.ALL<\/code> Kafka topic.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/a1dnxz9m7tlam1q\/64-output-topic-config.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/m949bos6ujt93vj\/64-output-topic-config.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>The input for this node was the single string coming from the JSON composer, which was used as the message payload for messages produced to the topic.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/agyxr6vd6o8q4u0\/65-output-topic-input.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/slqtu8slc94zew7\/65-output-topic-input.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>And that was the whole flow.<\/p>\n<p>To recap, the flow:<\/p>\n<ul>\n<li>consumed string messages from two of my stock price Kafka topics<\/li>\n<li>converted the JSON string messages to structured columnar data<\/li>\n<li>joined the two sets of data using the datetime value in each message<\/li>\n<li>flattened the joined columnar data back into a JSON string<\/li>\n<li>produced the JSON string messages to a new Kafka topic<\/li>\n<\/ul>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/hib78lotup4yabf\/66-flow.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/3xct7tfvv81buzr\/66-flow.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>I tested the flow by hitting the <strong>Run<\/strong> button.<\/p>\n<p>Because I didn&#8217;t tick the <strong>Continuous mode<\/strong> option in the Kafka consumer nodes, the flow ran through the messages on the topic until it didn&#8217;t see any new events for 30 seconds.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/n67em4heyq6mj50\/67-flow-run.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/0s3lf0zxvimulot\/67-flow-run.png?raw=1\"\/><\/a><\/p>\n<hr \/>\n<p>To confirm, I checked that a <code class=\"mqunicode\">STOCK.PRICES.ALL<\/code> topic was created in the Event Streams web interface, and that the messages on the topic all had combined share price events for each timestamp.<\/p>\n<p><a href=\"https:\/\/www.dropbox.com\/s\/gkxwfa0mdovy5zc\/68-event-streams-result.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/qkg91hp926x07pi\/68-event-streams-result.png?raw=1\"\/><\/a><br \/>\n<a href=\"https:\/\/www.dropbox.com\/s\/3q827zo083k3e31\/69-event-streams-result.png?dl=0\"><img decoding=\"async\" alt=\"screenshot\" class=\"screenshotthumb\" src=\"https:\/\/www.dropbox.com\/s\/m5vnfy8tjhocr5e\/69-event-streams-result.png?raw=1\"\/><\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this post, I share a step-by-step guide for how to use IBM DataStage to merge JSON messages from multiple different Apache Kafka topics, into a single joined-up stream of events.<\/p>\n","protected":false},"author":1,"featured_media":4743,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[593,582,583,584],"class_list":["post-4740","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ibm","tag-apachekafka","tag-eventstreams","tag-ibmeventstreams","tag-kafka"],"_links":{"self":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4740","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4740"}],"version-history":[{"count":0,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/posts\/4740\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=\/wp\/v2\/media\/4743"}],"wp:attachment":[{"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4740"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4740"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dalelane.co.uk\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4740"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}