{"id":19133309,"url":"https://github.com/kkgthb/salesforce-flow-apex-defined-data-types-examples","last_synced_at":"2025-11-12T21:02:19.745Z","repository":{"id":164101889,"uuid":"190245420","full_name":"kkgthb/Salesforce-Flow-Apex-Defined-Data-Types-Examples","owner":"kkgthb","description":"Apex code to support the blog","archived":false,"fork":false,"pushed_at":"2019-08-20T16:06:07.000Z","size":183,"stargazers_count":9,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-01-03T12:12:12.568Z","etag":null,"topics":["apex","salesforce","salesforce-admins","salesforce-developers","salesforce-flow"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kkgthb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-06-04T17:04:49.000Z","updated_at":"2023-10-11T12:18:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"7467573c-28a5-4862-b87b-de2d618cd597","html_url":"https://github.com/kkgthb/Salesforce-Flow-Apex-Defined-Data-Types-Examples","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkgthb%2FSalesforce-Flow-Apex-Defined-Data-Types-Examples","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkgthb%2FSalesforce-Flow-Apex-Defined-Data-Types-Examples/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkgthb%2FSalesforce-Flow-Apex-Defined-Data-Types-Examples/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkgthb%2FSalesforce-Flow-Apex-Defined-Data-Types-Examples/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kkgthb","download_url":"https://codeload.github.com/kkgthb/Salesforce-Flow-Apex-Defined-Data-Types-Examples/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240214563,"owners_count":19766263,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apex","salesforce","salesforce-admins","salesforce-developers","salesforce-flow"],"created_at":"2024-11-09T06:21:59.124Z","updated_at":"2025-10-14T12:11:19.669Z","avatar_url":"https://github.com/kkgthb.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Salesforce Flow Apex-Defined Data Types (code examples)\n\nContained within is a bit more Apex code to support my blog [Katie Kodes](https://katiekodes.com).\n\nThis is to help Salesforce Developers play around with Apex-Defined Data Types for Salesforce Flow, as used for calling out to HTTP APIs, in a bit more depth than is presented at my companion blog article, which I recommend reading first:\n\n\u003e [Tutorial: Flow Apex-Defined Data Types for Salesforce Admins](https://katiekodes.com/flow-apex-defined-data-types/)\n\nMany thanks to [Ultimate Courses's public API directory](https://github.com/public-apis/public-apis) for helping me find the 4 examples _([YesNo](#yesno), [Shouting As A Service](#shouting-as-a-service), [Indian Cities](#indian-cities), and [U.S. Zip Codes By City](#zip-codes-for-a-us-city))_.\n\n## HTTPMockFactory class\n\nYou'll need this class for any of the examples.  _(Each example also has 3-4 more classes you need to create, including a unit test to run, and some Anonymous Apex to execute.)_\n\n```java\npublic class HTTPMockFactory implements HttpCalloutMock {\n    protected Integer code;\n    protected String status;\n    protected String body;\n    protected Map\u003cString, String\u003e responseHeaders;\n    public HTTPMockFactory(Integer code, String status, String body, Map\u003cString, String\u003e responseHeaders) {\n        this.code = code;\n        this.status = status;\n        this.body = body;\n        this.responseHeaders = responseHeaders;\n    }\n    public HTTPResponse respond(HTTPRequest req) {\n        HttpResponse res = new HttpResponse();\n        for (String key : this.responseHeaders.keySet()) {\n            res.setHeader(key, this.responseHeaders.get(key));\n        }\n        res.setBody(this.body);\n        res.setStatusCode(this.code);\n        res.setStatus(this.status);\n        return res;\n    }\n}\n```\n\n---\n\n## YesNo\n\nReturn a random yes/no/maybe, and accompanying image, thanks to https://yesno.wtf/#api.\n\nYesNo always returns JSON-formatted text representing _just one_ object with 3 properties, like this:\n\n```json\n{\n    \"answer\":\"no\",\n    \"forced\":false,\n    \"image\":\"https://yesno.wtf/assets/no/10-d5ddf3f82134e781c1175614c0d2bab2.gif\"\n}\n```\n\n### YesNo class\n\nThis class allows YesNo to be used as a Flow Variable data type.\n\n```java\npublic class YesNo {\n    @AuraEnabled @InvocableVariable public String answer;\n    @AuraEnabled @InvocableVariable public Boolean forced;\n    @AuraEnabled @InvocableVariable public String image;\n}\n```\n\n### YesNoGenerator class\n\nThis class allows getYesNo(), a.k.a. \"Get YesNo,\" to be used as an Invocable Apex Method in a Flow.\n\nFlow seems to effectively \"`[0]`\" its return value and treat it as a single returned object -- hence declaring the return type of `getYesNo()` to be a `List` of `YesNo`s rather than a single `YesNo`.\n\n```java\npublic class YesNoGenerator {\n    @InvocableMethod(label='Get YesNo' description='Returns a response from the public API YesNo.wtf')\n    public static List\u003cYesNo\u003e getYesNo() {\n        List\u003cYesNo\u003e yesNo = new List\u003cYesNo\u003e{getRandomYesNo()};\n        return yesNo;\n    }\n    \n    private static YesNo getRandomYesNo() {\n        YesNo yn = new YesNo();\n        Http http = new Http();\n        HttpRequest request = new HttpRequest();\n        request.setEndpoint('https://yesno.wtf/api');\n        request.setMethod('GET');\n        HttpResponse response = http.send(request);\n        // If the request is successful, parse the JSON response.\n        if (response.getStatusCode() == 200) {\n            // Deserialize the JSON string into collections of primitive data types.\n            yn = (YesNo)JSON.deserialize(response.getBody(), YesNo.class);\n        }\n        return yn;\n    }\n}\n```\n\n### TestYesNoGenerator class\n\n```java\n@isTest\npublic class TestYesNoGenerator {\n    static testMethod void testYesNoGenerator() {\n        HttpMockFactory mock = new HttpMockFactory(\n            200, \n            'OK', \n            '{'+\n            '\"answer\":\"no\"'\n            +','\n            +'\"forced\":false'\n            +','\n            +'\"image\":\"https://yesno.wtf/assets/no/10-d5ddf3f82134e781c1175614c0d2bab2.gif\"'\n            +'}', \n            new Map\u003cString,String\u003e()\n        );\n        Test.setMock(HttpCalloutMock.class, mock);\n        Test.startTest();\n        List\u003cYesNo\u003e returnedYNs = YesNoGenerator.getYesNo();\n        Test.stopTest();\n        System.assert(!returnedYNs.isEmpty());\n        YesNo returnedYN = returnedYNs[0];\n        System.assertEquals('no',returnedYN.answer);\n    }\n}\n```\n\n\n### Anonymous Apex for actually testing the yes/no/maybe API live\n\n```java\nSystem.assert(FALSE, YesNoGenerator.getYesNo()[0]);\n```\n\nYou're expecting an error message with a Yes/No/Maybe and a GIF URL; something along the lines of:\n\n```\nLine: 1, Column: 1\nSystem.AssertException: Assertion Failed: YesNo:[answer=yes, forced=false, image=https://yesno.wtf/assets/yes/3-422e51268d64d78241720a7de52fe121.gif]\n```\n\nIf you get this error, you need to enable making callouts to https://yesno.wtf/api in your org's [Remote Site Settings](https://login.salesforce.com/one/one.app#/setup/SecurityRemoteProxy/home):\n\n```\nLine: 14, Column: 1\nSystem.CalloutException: Unauthorized endpoint, please check Setup-\u003eSecurity-\u003eRemote site settings. endpoint = https://yesno.wtf/api\n```\n\n---\n\n## SHOUTING AS A SERVICE\n\nUppercase your input text, thanks to http://shoutcloud.io\n\nShouting As A Service always returns JSON-formatted text representing _just one_ object with 2 properties, like this:\n\n```json\n{\n    \"INPUT\":\"helloWorld\",\n    \"OUTPUT\":\"HELLOWORLD\"\n}\n```\n\n**Security warning:**  this service is not available over HTTPS.\n\nIt is _quite_ possible for anyone on the internet to intercept the response from Shouting As A Service and replace it with a virus before it gets back to you.\n\nLikely?  Probably not.\n\nBut possible!  Play at your own risk.\n\n### AllCaps class\n\nThis class allows AllCaps to be used as a Flow Variable data type.\n\n```java\npublic class AllCaps {\n    @AuraEnabled @InvocableVariable public String input;\n    @AuraEnabled @InvocableVariable public String output;\n    \n    public AllCaps() {} // Needed for unit test code coverage\n}\n```\n\n### AllCapsGenerator class\n\nThis class allows getAllCaps(), a.k.a. \"All-Caps Your Text (INSECURE HTTP ONLY),\" to be used as an Invocable Apex Method in a Flow.\n\nAgain, flow seems to effectively \"`[0]`\" invocable methods' return values, so return a `List` of whatever you actually want to pass back to the flow.\n\nSimilarly, when invoked, it should be passed a single text-typed Flow Variable, not a \"collection\" / \"multiple values\"-enabled text-typed Flow Variable.\n\n```java\npublic class AllCapsGenerator {\n    @InvocableMethod(label='All-Caps Your Text (INSECURE HTTP ONLY)' description='Returns a response from the public API Shouting As A Service (INSECURE HTTP)')\n    public static List\u003cAllCaps\u003e getAllCaps(List\u003cString\u003e inputText) {\n        List\u003cAllCaps\u003e acText = new List\u003cAllCaps\u003e{getCapitalizedInputText(inputText[0])};\n        return acText;\n    }\n    \n    private static AllCaps getCapitalizedInputText(String inputString) {\n        AllCaps ac = new AllCaps();\n        Http http = new Http();\n        HttpRequest request = new HttpRequest();\n        request.setEndpoint('http://api.shoutcloud.io/V1/SHOUT'); // SECURITY VULNERABILITIES BECAUSE NOT AVAILABLE OVER HTTPS!  RUN AT YOUR OWN RISK!\n        request.setMethod('POST');\n        request.setHeader('Content-Type','application/json');\n        request.setBody('{\"INPUT\":\"'+inputString+'\"}');\n        HttpResponse response = http.send(request);\n        // If the request is successful, parse the JSON response.\n        if (response.getStatusCode() == 200) {\n            // Deserialize the JSON string into collections of primitive data types.\n            ac = (AllCaps)JSON.deserialize(response.getBody(), AllCaps.class);\n        }\n        return ac;\n    }\n}\n```\n\n### TestAllCapsGenerator class\n\n```java\n@isTest\npublic class TestAllCapsGenerator {\n    static testMethod void testAllCapsGenerator() {\n        String fixedInput = 'hello World';\n        HttpMockFactory mock = new HttpMockFactory(\n            200, \n            'OK', \n            '{'+\n            '\"INPUT\": \"hello World\"'\n            +','\n            +'\"OUTPUT\": \"HELLO WORLD\"'\n            +'}', \n            new Map\u003cString,String\u003e()\n        );\n        Test.setMock(HttpCalloutMock.class, mock);\n        Test.startTest();\n        List\u003cAllCaps\u003e returnedACs = AllCapsGenerator.getAllCaps(new List\u003cString\u003e{fixedInput});\n        Test.stopTest();\n        System.assert(!returnedACs.isEmpty());\n        AllCaps returnedAC = returnedACs[0];\n        System.assertEquals('HELLO WORLD',returnedAC.output);\n    }\n    \n    static testMethod void codeCoverageForAttributeOnlyClasses() {\n        AllCaps theAllCaps = new AllCaps();\n    }\n}\n```\n\n### Anonymous Apex for actually testing the shouting API live\n\n```java\nSystem.assert(FALSE, AllCapsGenerator.getAllCaps(new List\u003cString\u003e{('hi')})[0]);\n```\n\nYou're expecting an error message of:\n\n```\nLine: 1, Column: 1\nSystem.AssertException: Assertion Failed: AllCaps:[input=hi, output=HI]\n```\n\nIf you get this error, you need to enable making callouts to http://api.shoutcloud.io/V1/SHOUT in your org's [Remote Site Settings](https://login.salesforce.com/one/one.app#/setup/SecurityRemoteProxy/home):\n\n```\nLine: 16, Column: 1\nSystem.CalloutException: Unauthorized endpoint, please check Setup-\u003eSecurity-\u003eRemote site settings. endpoint = http://api.shoutcloud.io/V1/SHOUT\n```\n\n---\n\n## Indian Cities\n\nReturn a random Indian city thanks to https://indian-cities-api-nocbegfhqg.now.sh/\n\nIndian Cities always returns JSON-formatted text representing _a **list** of objects_ with 3 properties _apiece_, like this:\n\n```json\n[\n    {\n        \"City\":\"Shansha\",\n        \"State\":\"HimachalPradesh\",\n        \"District\":\"Lahual\"\n    },\n    {\n        \"City\":\"Kardang\",\n        \"State\":\"HimachalPradesh\",\n        \"District\":\"Lahual\"\n    }\n]\n```\n\n### IndianCity class\n\nThis class allows IndianCity to be used as a Flow Variable data type.\n\nAgain, flow seems to effectively \"`[0]`\" invocable methods' return values, so return a `List` of whatever you actually want to pass back to the flow.\n\n```java\npublic class IndianCity {\n    @AuraEnabled @InvocableVariable public String city;\n    @AuraEnabled @InvocableVariable public String state;\n    @AuraEnabled @InvocableVariable public String district;\n    \n    public IndianCity() {} // Needed for unit test code coverage\n}\n```\n\n### IndianCityGenerator class\n\nThis class allows getIndianCity(), a.k.a. \"Get Random Indian City,\" to be used as an Invocable Apex Method in a Flow.\n\n```java\npublic class IndianCityGenerator {\n    \n    private static List\u003cIndianCity\u003e allIndianCities = new List\u003cIndianCity\u003e();\n    \n    @InvocableMethod(label='Get Random Indian City' description='Returns a response from the public Indian Cities API')\n    public static List\u003cIndianCity\u003e getIndianCity() {\n        List\u003cIndianCity\u003e indCit = new List\u003cIndianCity\u003e{getRandomIndianCity()};\n            return indCit;\n    }\n    \n    private static IndianCity getRandomIndianCity() {\n        if ( allIndianCities.isEmpty() ) {\n            Http http = new Http();\n            HttpRequest request = new HttpRequest();\n            request.setEndpoint('https://indian-cities-api-nocbegfhqg.now.sh/cities');\n            request.setMethod('GET');\n            HttpResponse response = http.send(request);\n            // If the request is successful, parse the JSON response.\n            if (response.getStatusCode() == 200) {\n                // Deserialize the JSON string into collections of primitive data types.\n                allIndianCities = (List\u003cIndianCity\u003e)JSON.deserialize(response.getBody(), List\u003cIndianCity\u003e.class);\n            }\n        }\n        if ( allIndianCities.isEmpty() ) {\n            return new IndianCity();\n        } else {\n            Integer randomNumber = Math.mod(Math.abs(Crypto.getRandomLong().intValue()),allIndianCities.size());\n            return allIndianCities[randomNumber]; \n        }\n    }\n}\n```\n\n### TestIndianCityGenerator class\n\n```java\n@isTest\npublic class TestIndianCityGenerator {\n    static testMethod void testIndianCityGenerator() {\n        HttpMockFactory mock = new HttpMockFactory(\n            200, \n            'OK', \n            '[{'+\n            '\"City\":\"Shansha\",\"State\":\"Himachal Pradesh\",\"District\":\"Lahual\"}'+\n            ','+\n            '{\"City\":\"Kardang\",\"State\":\"Himachal Pradesh\",\"District\":\"Lahual\"'+\n            '}]', \n            new Map\u003cString,String\u003e()\n        );\n        Test.setMock(HttpCalloutMock.class, mock);\n        Test.startTest();\n        List\u003cIndianCity\u003e returnedCities = IndianCityGenerator.getIndianCity();\n        Test.stopTest();\n        System.assert(!returnedCities.isEmpty());\n        IndianCity returnedCity = returnedCities[0];\n        System.assert(String.isNotEmpty(returnedCity.city));\n        System.assert('Shansha|Kardang'.contains(returnedCity.city));\n    }\n    \n    static testMethod void codeCoverageForAttributeOnlyClasses() {\n        IndianCity theIndianCity = new IndianCity();\n    }\n}\n```\n\n### Anonymous Apex for actually testing the cities API live\n\n```java\nSystem.assert(FALSE, IndianCityGenerator.getIndianCity()[0]);\n```\n\nYou're expecting an error message with a random city, something along the lines of:\n\n```\nLine: 1, Column: 1\nSystem.AssertException: Assertion Failed: IndianCity:[city=Tuljapur, district=Osmanabad, state=Maharashtra]\n```\n\nIf you get this error, you need to enable making callouts to https://indian-cities-api-nocbegfhqg.now.sh/cities in your org's [Remote Site Settings](https://login.salesforce.com/one/one.app#/setup/SecurityRemoteProxy/home):\n\n```\nLine: 17, Column: 1\nSystem.CalloutException: Unauthorized endpoint, please check Setup-\u003eSecurity-\u003eRemote site settings. endpoint = https://indian-cities-api-nocbegfhqg.now.sh/cities\n```\n\n---\n\n## Zip Codes for a U.S. City\n\nInspired by Alex Edelstein's [official Salesforce video demo of Apex-Defined Data Types (from 12:53 to 19:29)](https://www.youtube.com/watch?v=oU0y38yf5qw\u0026t=766) and his accompanying \"Unofficial Salesforce\" blog posts parts [1](https://unofficialsf.com/part-1-manipulate-complex-internet-data-in-flow-without-code/) \u0026 [2](https://unofficialsf.com/part-2-manipulate-complex-internet-data-in-flow-without-code/) and [video](https://www.youtube.com/watch?v=3xH1YLh5L7s), I decided to add one more example here that leverages Apex-Defined Data Types the way Salesforce _intended_ them to work:  with the parsing of \"nested inner JSON classes\" handed over to administrators within Flow, rather than being done by developers within Apex.\n\nFor this example, I chose an API from https://zippopotam.us that, provided a country like `us` and a state abbreviation like `wi` and a phrase like `beloit`, can search for all cities in that state containing the phrase `beloit` and return all zip codes assigned to Beloit, Wisconsin, USA.\n\nZippopotamus always returns JSON-formatted text with a more complex, nested structure than we've seen before, like this call to https://api.zippopotam.us/us/wi/beloit:\n\n```json\n{\n  \"country abbreviation\": \"US\",\n  \"places\": [\n    {\n      \"place name\": \"Beloit\",\n      \"longitude\": \"-89.086\",\n      \"post code\": \"53511\",\n      \"latitude\": \"42.562\"\n    },\n    {\n      \"place name\": \"Beloit\",\n      \"longitude\": \"-89.0728\",\n      \"post code\": \"53512\",\n      \"latitude\": \"42.6698\"\n    }\n  ],\n  \"country\": \"United States\",\n  \"place name\": \"Beloit\",\n  \"state\": \"Wisconsin\",\n  \"state abbreviation\": \"WI\"\n}\n```\n\nOf course, we could build a sophisticated tool that lets users enter their own city, state, and country, but there're a lot of issues to handle that I'd rather not go into.\n\nFor example, http://api.zippopotam.us/us/mn/Saint%20Paul returns data from 3 Minnesota cities containing `Saint Paul` in their names, the largest of which is Saint Paul itself, but attributes the overall list to the small suburb of Saint Paul Park:\n\n```json\n{\n  \"country abbreviation\": \"US\",\n  \"places\": [\"LOTS OF ZIPS FROM Saint Paul Park, South Saint Paul, Saint Paul, etc.\"],\n  \"country\": \"United States\",\n  \"place name\": \"Saint Paul Park\",\n  \"state\": \"Minnesota\",\n  \"state abbreviation\": \"MN\"\n}\n```\n\nTrying to search any city in France gets nasty quickly because the results are overwhelmingly filled with [CEDEX](https://en.wikipedia.org/wiki/Postal_codes_in_France#CEDEX) fake zip codes for businesses.\n\nObviously, this might not be the right API for you to use in real life!\n\nTo simplify things, in this example, we'll hard-code the \"live\" HTTP request to always ask about Mankato, Minnesota.\n\nWe'll do a \"mock\" in our test class with Moorhead, Minnesota just to make sure we didn't over-tailor our code to Mankato.\n\nThe code here will support an admin generating a flow that produces text like this:\n\n```\nBeloit\nWI\n\n53511\n53512\n```\n\nOf note is that it's the **admin's** responsibility to build out a complex Flow that loops over the zip codes and produces this `53511\u003cbr/\u003e53512\u003cbr/\u003e` content.\n\n### CityZip class\n\nThis class allows CityZip to be used as a Flow Variable data type.\n\nBe sure to avoid using underscores in the variable names -- Flow produces errors if you do.\n\n```java\npublic class CityZip {\n    @AuraEnabled @InvocableVariable public String countryabbreviation;\n    @AuraEnabled @InvocableVariable public List\u003cPlace\u003e places;\n    @AuraEnabled @InvocableVariable public String country;\n    @AuraEnabled @InvocableVariable public String placename;\n    @AuraEnabled @InvocableVariable public String state;\n    @AuraEnabled @InvocableVariable public String stateabbreviation;\n    \n    public CityZip() {} // Needed for unit test code coverage\n}\n```\n\n### Place class\n\nHowever, we also had a list of objects embedded in the JSON under the key `places`.\n\nI decided to call an Apex class that could be used to represent one of these objects `Place`.  `CityZipPlace` would have done just as nicely, though.\n\n`Place` needs to be an \"outer,\" standalone Apex class, not an \"inner,\" nested class inside `CityZip`, so that `Place` can be seen by Flow.\n\nBe sure to avoid using underscores in the variable names -- Flow produces errors if you do.\n\n```java\npublic class Place {\n    @AuraEnabled @InvocableVariable public String placename;\n    @AuraEnabled @InvocableVariable public String longitude;\n    @AuraEnabled @InvocableVariable public String postcode;\n    @AuraEnabled @InvocableVariable public String latitude;\n    \n    public Place() {} // Needed for unit test code coverage\n}\n```\n\n### CityZipGenerator class\n\nThis class allows getCityZip(), a.k.a. \"Get CityZip Mankato,\" to be used as an Invocable Apex Method in a Flow.\n\nAgain, flow seems to effectively \"`[0]`\" invocable methods' return values, so return a `List` of whatever you actually want to pass back to the flow.\n\nNote our use of a Regular Expression replacement to eliminate any spaces in the JSON \"keys,\" since we can't put spaces into the Apex-Defined Data Type attribute names to which we're deserializing.\n\n_(At first I tried replacing the spaces with underscores for clarity, but it turns out Flow doesn't like variable names with underscores in them.)_\n\n```java\npublic class CityZipGenerator {\n    private static Pattern spaceInJSONKey = pattern.compile('(?\u003c=\"[a-z]+) (?=[a-z]+\": )');\n    \n    @InvocableMethod(label='Get CityZip Mankato' description='Returns a response about Mankato from the public API Zippopotam.us')\n    public static List\u003cCityZip\u003e getCityZip() {\n        List\u003cCityZip\u003e czs = new List\u003cCityZip\u003e{getMankatoCityZip()};\n        return czs;\n    }\n    \n    private static CityZip getMankatoCityZip() {\n        CityZip cz = new CityZip();\n        Http http = new Http();\n        HttpRequest request = new HttpRequest();\n        request.setEndpoint('https://api.zippopotam.us/us/mn/mankato');\n        request.setMethod('GET');\n        HttpResponse response = http.send(request);\n        // If the request is successful, parse the JSON response.\n        if (response.getStatusCode() == 200) {\n            // Deserialize the JSON string into collections of primitive data types.\n            cz = (CityZip)JSON.deserialize(\n                spaceInJSONKey.matcher(response.getBody()).replaceAll(''),\n                CityZip.class\n            );\n        }\n        return cz;\n    }\n}\n```\n\n### TestCityZipGenerator class\n\n```java\n@isTest\npublic class TestCityZipGenerator {\n    static testMethod void testCityZipGenerator() {\n        HttpMockFactory mock = new HttpMockFactory(\n            200, \n            'OK', \n            '{\"country abbreviation\": \"US\", \"places\": '+\n            '[{\"place name\": \"Moorhead\", \"longitude\": \"-96.7572\", '+\n            '\"post code\": \"56560\", \"latitude\": \"46.8677\"}, '+\n            '{\"place name\": \"Moorhead\", \"longitude\": \"-96.5062\", '+\n            '\"post code\": \"56561\", \"latitude\": \"46.89\"}, '+\n            '{\"place name\": \"Moorhead\", \"longitude\": \"-96.5062\", '+\n            '\"post code\": \"56562\", \"latitude\": \"46.89\"}, '+\n            '{\"place name\": \"Moorhead\", \"longitude\": \"-96.5062\", '+\n            '\"post code\": \"56563\", \"latitude\": \"46.89\"}], '+\n            '\"country\": \"United States\", \"place name\": \"Moorhead\", '+\n            '\"state\": \"Minnesota\", \"state abbreviation\": \"MN\"}', \n            new Map\u003cString,String\u003e()\n        );\n        Test.setMock(HttpCalloutMock.class, mock);\n        Test.startTest();\n        List\u003cCityZip\u003e returnedCZs = CityZipGenerator.getCityZip();\n        Test.stopTest();\n        System.assert(!returnedCZs.isEmpty());\n        CityZip returnedCZ = returnedCZs[0];\n        System.assertEquals('MN',returnedCZ.stateabbreviation);\n        System.assertEquals('Moorhead',returnedCZ.placename);\n        System.assert(returnedCZ.places[2].postcode.startsWith('5656'));\n    }\n    \n    static testMethod void codeCoverageForAttributeOnlyClasses() {\n        CityZip theCityZip = new CityZip();\n        Place thePlace = new Place();\n    }\n}\n```\n\n### Anonymous Apex for actually testing the Zippopotam.us API live\n\n```java\nSystem.assert(FALSE, CityZipGenerator.getCityZip()[0]);\n```\n\nYou're expecting an error message with information about Mankato, Minnesota, USA; something along the lines of:\n\n```\nLine: 1, Column: 1\nSystem.AssertException: Assertion Failed: CityZip:[country=United States, countryabbreviation=US, placename=Mankato, places=(Place:[latitude=44.1538, longitude=-93.996, placename=Mankato, postcode=56001], Place:[latitude=44.056, longitude=-94.0698, placename=Mankato, postcode=56002], Place:[latitude=44.2172, longitude=-94.0942, placename=Mankato, postcode=56003], Place:[latitude=44.056, longitude=-94.0698, placename=Mankato, postcode=56006]), state=Minnesota, stateabbreviation=MN]\n```\n\nIf you get this error, you need to enable making callouts to https://api.zippopotam.us in your org's [Remote Site Settings](https://login.salesforce.com/one/one.app#/setup/SecurityRemoteProxy/home) _(note that you can chop off `/us/mn/mankato`)_:\n\n```\nLine: 14, Column: 1\nSystem.CalloutException: Unauthorized endpoint, please check Setup-\u003eSecurity-\u003eRemote site settings. endpoint = https://api.zippopotam.us/us/mn/mankato\n```\n\n### The Flow\n\nFinally, you need an admin to build a flow to put this data on a screen.\n\nThe 3 previous examples are all variations on [Tutorial: Flow Apex-Defined Data Types for Salesforce Admins](https://katiekodes.com/flow-apex-defined-data-types/), but this one is a little different.\n\nThere's nothing that can be added to a Screen element in Flow Builder that natively displays \"repeating\" or \"list-like\" data in an attractive fashion.\n\nA relatively simple workaround to this problem, with credit to [Jen Lee's \"Build A Search Tool Using Flow\"](https://jenwlee.wordpress.com/2017/05/30/build-a-search-tool-using-flow/), is to use a Loop element to populate the contents of a single Text-typed variable by concatenating elements of each record in a list with the HTML line break `\u003cbr/\u003e` between them.\n\nI'll screenshot the whole process for a \"part 2\" blog post at a later date, but for now, here's the XML-formatted code I downloaded from my demo org as `MyFirstFlow.flow-meta.xml`.  When it's run, its output is a simple:\n\n```\nMankato\nMN\n\n56001\n56002\n56003\n56006\n```\n\nImport it into your org, open the new Flow it creates, and give it a run!\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cFlow xmlns=\"http://soap.sforce.com/2006/04/metadata\"\u003e\n    \u003cactionCalls\u003e\n        \u003cname\u003eGet_City\u003c/name\u003e\n        \u003clabel\u003eGet City\u003c/label\u003e\n        \u003clocationX\u003e149\u003c/locationX\u003e\n        \u003clocationY\u003e50\u003c/locationY\u003e\n        \u003cactionName\u003eCityZipGenerator\u003c/actionName\u003e\n        \u003cactionType\u003eapex\u003c/actionType\u003e\n        \u003cconnector\u003e\n            \u003ctargetReference\u003eDump_myCity_Places_Into_Standalone_placeList\u003c/targetReference\u003e\n        \u003c/connector\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.country\u003c/assignToReference\u003e\n            \u003cname\u003ecountry\u003c/name\u003e\n        \u003c/outputParameters\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.countryabbreviation\u003c/assignToReference\u003e\n            \u003cname\u003ecountryabbreviation\u003c/name\u003e\n        \u003c/outputParameters\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.placename\u003c/assignToReference\u003e\n            \u003cname\u003eplacename\u003c/name\u003e\n        \u003c/outputParameters\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.places\u003c/assignToReference\u003e\n            \u003cname\u003eplaces\u003c/name\u003e\n        \u003c/outputParameters\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.state\u003c/assignToReference\u003e\n            \u003cname\u003estate\u003c/name\u003e\n        \u003c/outputParameters\u003e\n        \u003coutputParameters\u003e\n            \u003cassignToReference\u003emyCity.stateabbreviation\u003c/assignToReference\u003e\n            \u003cname\u003estateabbreviation\u003c/name\u003e\n        \u003c/outputParameters\u003e\n    \u003c/actionCalls\u003e\n    \u003cassignments\u003e\n        \u003cname\u003eAdd_to_allZipCodesAsText_from_Template_RenderCurPlaceDetail\u003c/name\u003e\n        \u003clabel\u003eAdd to allZipCodesAsText from Template RenderCurPlaceDetail\u003c/label\u003e\n        \u003clocationX\u003e390\u003c/locationX\u003e\n        \u003clocationY\u003e192\u003c/locationY\u003e\n        \u003cassignmentItems\u003e\n            \u003cassignToReference\u003eallZipCodesAsText\u003c/assignToReference\u003e\n            \u003coperator\u003eAssign\u003c/operator\u003e\n            \u003cvalue\u003e\n                \u003celementReference\u003eRenderCurPlaceDetail\u003c/elementReference\u003e\n            \u003c/value\u003e\n        \u003c/assignmentItems\u003e\n        \u003cconnector\u003e\n            \u003ctargetReference\u003ePopulate_allZipCodesAsText_from_placeList\u003c/targetReference\u003e\n        \u003c/connector\u003e\n    \u003c/assignments\u003e\n    \u003cassignments\u003e\n        \u003cname\u003eDump_myCity_Places_Into_Standalone_placeList\u003c/name\u003e\n        \u003clabel\u003eDump myCity.Places Into Standalone placeList\u003c/label\u003e\n        \u003clocationX\u003e256\u003c/locationX\u003e\n        \u003clocationY\u003e50\u003c/locationY\u003e\n        \u003cassignmentItems\u003e\n            \u003cassignToReference\u003eplaceList\u003c/assignToReference\u003e\n            \u003coperator\u003eAssign\u003c/operator\u003e\n            \u003cvalue\u003e\n                \u003celementReference\u003emyCity.places\u003c/elementReference\u003e\n            \u003c/value\u003e\n        \u003c/assignmentItems\u003e\n        \u003cconnector\u003e\n            \u003ctargetReference\u003ePopulate_allZipCodesAsText_from_placeList\u003c/targetReference\u003e\n        \u003c/connector\u003e\n    \u003c/assignments\u003e\n    \u003cinterviewLabel\u003eMyFirstFlow {!$Flow.CurrentDateTime}\u003c/interviewLabel\u003e\n    \u003clabel\u003eMyFirstFlow\u003c/label\u003e\n    \u003cloops\u003e\n        \u003cname\u003ePopulate_allZipCodesAsText_from_placeList\u003c/name\u003e\n        \u003clabel\u003ePopulate allZipCodesAsText from placeList\u003c/label\u003e\n        \u003clocationX\u003e389\u003c/locationX\u003e\n        \u003clocationY\u003e50\u003c/locationY\u003e\n        \u003cassignNextValueToReference\u003ecurPlace\u003c/assignNextValueToReference\u003e\n        \u003ccollectionReference\u003eplaceList\u003c/collectionReference\u003e\n        \u003citerationOrder\u003eAsc\u003c/iterationOrder\u003e\n        \u003cnextValueConnector\u003e\n            \u003ctargetReference\u003eAdd_to_allZipCodesAsText_from_Template_RenderCurPlaceDetail\u003c/targetReference\u003e\n        \u003c/nextValueConnector\u003e\n        \u003cnoMoreValuesConnector\u003e\n            \u003ctargetReference\u003eCity_Screen\u003c/targetReference\u003e\n        \u003c/noMoreValuesConnector\u003e\n    \u003c/loops\u003e\n    \u003cprocessMetadataValues\u003e\n        \u003cname\u003eBuilderType\u003c/name\u003e\n        \u003cvalue\u003e\n            \u003cstringValue\u003eLightningFlowBuilder\u003c/stringValue\u003e\n        \u003c/value\u003e\n    \u003c/processMetadataValues\u003e\n    \u003cprocessMetadataValues\u003e\n        \u003cname\u003eOriginBuilderType\u003c/name\u003e\n        \u003cvalue\u003e\n            \u003cstringValue\u003eLightningFlowBuilder\u003c/stringValue\u003e\n        \u003c/value\u003e\n    \u003c/processMetadataValues\u003e\n    \u003cprocessType\u003eFlow\u003c/processType\u003e\n    \u003cscreens\u003e\n        \u003cname\u003eCity_Screen\u003c/name\u003e\n        \u003clabel\u003eCity Screen\u003c/label\u003e\n        \u003clocationX\u003e554\u003c/locationX\u003e\n        \u003clocationY\u003e49\u003c/locationY\u003e\n        \u003callowBack\u003etrue\u003c/allowBack\u003e\n        \u003callowFinish\u003efalse\u003c/allowFinish\u003e\n        \u003callowPause\u003efalse\u003c/allowPause\u003e\n        \u003cfields\u003e\n            \u003cname\u003eshowInfoAboutCity\u003c/name\u003e\n            \u003cfieldText\u003e\u0026lt;p\u0026gt;\u0026lt;b\u0026gt;{!myCity.placename}\u0026lt;/b\u0026gt;\u0026lt;/p\u0026gt;\u0026lt;p\u0026gt;\u0026lt;b\u0026gt;{!myCity.stateabbreviation}\u0026lt;/b\u0026gt;\u0026lt;/p\u0026gt;\u0026lt;p\u0026gt;\u0026lt;br\u0026gt;\u0026lt;/p\u0026gt;\u0026lt;p\u0026gt;{!allZipCodesAsText}\u0026lt;/p\u0026gt;\u003c/fieldText\u003e\n            \u003cfieldType\u003eDisplayText\u003c/fieldType\u003e\n        \u003c/fields\u003e\n        \u003cshowFooter\u003efalse\u003c/showFooter\u003e\n        \u003cshowHeader\u003efalse\u003c/showHeader\u003e\n    \u003c/screens\u003e\n    \u003cstartElementReference\u003eGet_City\u003c/startElementReference\u003e\n    \u003cstatus\u003eDraft\u003c/status\u003e\n    \u003ctextTemplates\u003e\n        \u003cname\u003eRenderCurPlaceDetail\u003c/name\u003e\n        \u003ctext\u003e{!allZipCodesAsText}{!curPlace.postcode}\u0026lt;br/\u0026gt;\u003c/text\u003e\n    \u003c/textTemplates\u003e\n    \u003cvariables\u003e\n        \u003cname\u003eallZipCodesAsText\u003c/name\u003e\n        \u003cdataType\u003eString\u003c/dataType\u003e\n        \u003cisCollection\u003efalse\u003c/isCollection\u003e\n        \u003cisInput\u003efalse\u003c/isInput\u003e\n        \u003cisOutput\u003efalse\u003c/isOutput\u003e\n    \u003c/variables\u003e\n    \u003cvariables\u003e\n        \u003cname\u003ecurPlace\u003c/name\u003e\n        \u003capexClass\u003ePlace\u003c/apexClass\u003e\n        \u003cdataType\u003eApex\u003c/dataType\u003e\n        \u003cisCollection\u003efalse\u003c/isCollection\u003e\n        \u003cisInput\u003efalse\u003c/isInput\u003e\n        \u003cisOutput\u003efalse\u003c/isOutput\u003e\n    \u003c/variables\u003e\n    \u003cvariables\u003e\n        \u003cname\u003emyCity\u003c/name\u003e\n        \u003capexClass\u003eCityZip\u003c/apexClass\u003e\n        \u003cdataType\u003eApex\u003c/dataType\u003e\n        \u003cisCollection\u003efalse\u003c/isCollection\u003e\n        \u003cisInput\u003efalse\u003c/isInput\u003e\n        \u003cisOutput\u003efalse\u003c/isOutput\u003e\n    \u003c/variables\u003e\n    \u003cvariables\u003e\n        \u003cname\u003eplaceList\u003c/name\u003e\n        \u003capexClass\u003ePlace\u003c/apexClass\u003e\n        \u003cdataType\u003eApex\u003c/dataType\u003e\n        \u003cisCollection\u003etrue\u003c/isCollection\u003e\n        \u003cisInput\u003efalse\u003c/isInput\u003e\n        \u003cisOutput\u003efalse\u003c/isOutput\u003e\n    \u003c/variables\u003e\n\u003c/Flow\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkkgthb%2Fsalesforce-flow-apex-defined-data-types-examples","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkkgthb%2Fsalesforce-flow-apex-defined-data-types-examples","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkkgthb%2Fsalesforce-flow-apex-defined-data-types-examples/lists"}