Skip to main content

Importing XML into Objects

The %XML.ReaderOpens in a new tab class enables you to import XML documents into InterSystems IRIS® data platform objects.

Note:

The XML declaration of any XML document that you use should indicate the character encoding of that document, and the document should be encoded as declared. If the character encoding is not declared, InterSystems IRIS uses the defaults described in Character Encoding of Input and Output. If these defaults are not correct, modify the XML declaration so that it specifies the character set actually used.

You can also use %XML.ReaderOpens in a new tab to read an arbitrary XML document and return a DOM (Document Object Model); see Representing an XML Document as a DOM.

Overview

InterSystems IRIS provides tools for reading an XML document and creating one or more instances of XML-enabled InterSystems IRIS objects that correspond to elements of that document. The essential requirements are as follows:

The %XML.ReaderOpens in a new tab class works with the methods provided by the %XML.AdaptorOpens in a new tab class to do the following:

  • It parses and validates the incoming XML document using the InterSystems IRIS SAX interface. The validation can include either DTD or XML Schema validation.

  • It determines if any XML-enabled objects are correlated with the elements contained within the XML document and creates in-memory instances of these objects as it reads the document.

Note that the object instances created by %XML.ReaderOpens in a new tab are not stored within the database; they are in-memory objects. If you want to store objects within the database, you must call the %Save() method (for persistent objects) or copy the relevant property values to a persistent object and save it. The application must also decide when to insert new data and when to update existing data; %XML.ReaderOpens in a new tab has no way of making this distinction.

The following Terminal session shows a simple example. Here we read an XML file into a new object, examine that object, and then save that object:

GXML>Set reader = ##class(%XML.Reader).%New()
 
GXML>Set file="c:\sample-input.xml"
 
GXML>Set status = reader.OpenFile(file)
 
GXML>Write status
1
GXML>Do reader.Correlate("Person","GXML.Person")
 
GXML>Do reader.Next(.object,.status)
 
GXML>Write status
1
GXML>Write object.Name
Worthington,Jeff R.
GXML>Write object.Doctors.Count()
2
GXML>Do object.%Save()

This example uses the following sample XML file:

<Person GroupID="90455">
  <Name>Worthington,Jeff R.</Name>
  <DOB>1976-11-03</DOB>
  <Address>
    <City>Elm City</City>
    <Zip>27820</Zip>
  </Address>
  <Doctors>
    <Doctor>
      <Name>Best,Nora A.</Name>
    </Doctor>
    <Doctor>
      <Name>Weaver,Dennis T.</Name>
    </Doctor>
  </Doctors>
</Person>

Importing Objects

Overall Method Structure

Your method should do some or all of the following, in this order:

  1. Create an instance of the %XML.ReaderOpens in a new tab class.

  2. Optionally specify the Format property of this instance, to specify the format of the file that you are importing; see Reader Properties.

    By default, InterSystems IRIS assumes that XML files are in literal format. If your file is in SOAP-encoded format, you must indicate this so that the file can be read correctly.

  3. Optionally set other properties of this instance; see Reader Properties.

  4. Open the source. To do so, use one of the following methods of %XML.ReaderOpens in a new tab:

    • OpenFile() — Opens a file.

    • OpenStream() — Opens a stream.

    • OpenString() — Opens a string.

    • OpenURL() — Opens a URL.

      If the URL uses HTTPS, see Accessing a Document at an HTTPS URL.

    In each case, you can optionally specify a second argument for the method, to override the value of the Format property.

  5. Correlate one or more XML element names in this file with InterSystems IRIS XML-enabled classes that have the corresponding structures. There are two ways to do this:

    • Use the Correlate() method, which has the following signature:

      method Correlate(element As %String, 
                       class As %String, 
                       namespace As %String) {}

      Where element is an XML element name, class is an InterSystems IRIS class name (with package), and namespace is an optional namespace URI.

      If you use the namespace argument, the match is restricted to the specified element name in the specified namespace. If you specify the namespace argument as "", that matches the default namespace given in the Next() method. If you do not use the namespace argument, then only the element name is used for the match.

      Tip:

      You can call the Correlate() method repeatedly to correlate more than one element, although the examples in this topic show only one correlation.

    • Use the CorrelateRoot() method, which has the following signature:

      method CorrelateRoot(class As %String) {}

      Where class is an InterSystems IRIS class name (with package). This method specifies that the root element of the XML document is correlated to the specified class.

  6. Instantiate the class instances as follows:

    • If you used Correlate(), loop through the correlated elements in the file, one element at a time. Within the loop, use the Next() method, which has the following signature:

      method Next(ByRef oref As %ObjectHandle, 
                  ByRef sc As %Status, 
                  namespace As %String = "") as %Integer {}

      Where oref is the object created by the method, sc is the status, and namespace is the default namespace for the file.

    • If you used CorrelateRoot(), call the Next() method once, which causes the correlated class to be instantiated.

    The Next() method returns 0 when it reaches the end of the file. If you call Next() again after this, you will loop through the objects in the file again, starting at the top of the file. (The correlations you specified are still in effect.)

Error Checking

Most of the methods mentioned in the previous section return a status. You should check the status after each step and quit if appropriate.

Basic Import Example

Consider the following XML file called test.xml:

<Root>
   <Person>
      <Name>Elvis Presley</Name>
   </Person>
   <Person>
      <Name>Johnny Carson</Name>
   </Person>
</Root>

We start by defining an XML-enabled class, MyApp.Person, that is an object representation of a person:

Class MyApp.Person Extends (%Persistent,%XML.Adaptor)
{
Parameter XMLNAME = "Person";

Property Name As %String;
}

To import this file into an instance of a MyAppPerson class, we could write the following method:

ClassMethod Import()
{
    // Create an instance of %XML.Reader
    Set reader = ##class(%XML.Reader).%New()

    // Begin processing of the file
    Set status = reader.OpenFile("c:\test.xml")
    If $$$ISERR(status) {do $System.Status.DisplayError(status)}

    // Associate a class name with the XML element name
    Do reader.Correlate("Person","MyApp.Person")
    
    // Read objects from xml file
    While (reader.Next(.object,.status)) {
        Write object.Name,!
    }
    
    // If error found during processing, show it
    If $$$ISERR(status) {do $System.Status.DisplayError(status)}

}

This method performs several tasks:

  • It parses the input file using the InterSystems IRIS SAX interface. This includes validating the document against its DTD or schema, if specified.

  • The Correlate() method associates the class MyApp.MyPerson with the XML element <Person>; each child element in <Person> becomes a property of MyPerson.

  • It reads each <Person> element from the input file until there are none left.

  • Finally, if the loop terminates because of an error, the error is displayed on the current output device.

As mentioned above, this example does not store objects to the database. Because MyPerson is a persistent object, you could do this by adding the following lines within the While loop:

    Set savestatus = object.%Save()
    If $$$ISERR(savestatus) {do $System.Status.DisplayError(savestatus)}

Accessing a Document at an HTTPS URL

For the OpenURL() method, if the document is at a URL that requires SSL/TLS, do the following:

  1. Use the Management Portal to create an SSL/TLS configuration that contains the details of the needed connection. For information, see InterSystems TLS Guide.

    This is a one-time step.

  2. When you use %XML.ReaderOpens in a new tab, set the SSLConfiguration property of the reader instance. For the value, specify the name of the SSL/TLS configuration that you created in the previous step.

Or, when you use %XML.ReaderOpens in a new tab, also do the following:

  1. Create an instance of %Net.HttpRequestOpens in a new tab.

  2. Set the SSLConfiguration property of that instance equal to the configuration name of the SSL/TLS configuration that you created in the Management Portal.

  3. Use that instance of %Net.HttpRequestOpens in a new tab as the third argument to OpenURL().

For example:

 set reader=##class(%XML.Reader).%New() 
 set httprequest=##class(%Net.HttpRequest).%New() 
 set httprequest.SSLConfiguration="mysslconfigname" 
 set status=reader.OpenURL("https://myurl",,httprequest) 

Accessing a Document When the Server Requires Authentication

If the server requires authentication, create an instance of %Net.HttpRequestOpens in a new tab and set the Username and Password properties of that instance. Also use SSL as described above (so also set the SSLConfiguration property). Then use that instance of %Net.HttpRequestOpens in a new tab as the third argument to OpenURL() as shown in the example above.

For more details on %Net.HttpRequestOpens in a new tab and authentication, see Providing Login Credentials in Sending HTTP Requests.

Checking for Required Elements and Attributes

By default, the Next() method does not check for the existence of elements and attributes that correspond to properties that are marked as Required. To cause the reader to check for the existence of such elements and attributes, set the CheckRequired property of the reader to 1 before calling Next(). The default value for this property is 0, for compatibility reasons.

If you set CheckRequired to 1, and if you call Next() and the imported XML is missing a required element or attribute, the Next() method sets the sc argument to an error code. For example:

SAMPLES>set next= reader.Next(.object,.status)
 
SAMPLES>w next
0
SAMPLES>d $system.Status.DisplayError(status)
 
ERROR #6318: Property required in XML document: ReqProp

Handling Unexpected Elements and Attributes

Because the source XML documents might contain unexpected elements and attributes, the %XML.AdaptorOpens in a new tab class provides parameters to specify how to react when you import such a document. For details, see Special Topics.

Controlling How Empty Elements and Attributes Are Imported

When you XML-enable an object, you specify how null values and empty strings are projected to XML (see Projecting Objects to XML).

One of the options is to set XMLIGNORENULL equal to "RUNTIME" (not case-sensitive) in the XML-enabled class. In this case, when you use %XML.ReaderOpens in a new tab to read an XML file and create InterSystems IRIS objects, InterSystems IRIS uses the value of the IgnoreNull property of the reader to determine how to handle empty elements or attributes, as follows:

  • If the IgnoreNull property of the reader is 0 (the default), and an element or attribute is empty, the corresponding property is set equal to $char(0)

  • If the IgnoreNull property of the reader is 1, and an element or attribute is empty, the corresponding property is not set and therefore equals ""

The IgnoreNull property of the reader has no effect unless the XMLIGNORENULL is "RUNTIME" in the XML-enabled class; the other possible values for XMLIGNORENULL are 0 (the default), 1, and "INPUTONLY"; see Projecting Objects to XML.

Example: IgnoreNull Is 0 (Default)

First, consider the following class:

Class EmptyStrings.Import Extends (%Persistent, %XML.Adaptor)
{

Parameter XMLNAME="Test";

///Reader will set IgnoreNull property
Parameter XMLIGNORENULL = "RUNTIME";

Property PropertyA As %String;

Property PropertyB As %String;

Property PropertyC As %String;

Property PropertyD As %String(XMLPROJECTION = "ATTRIBUTE");

Property PropertyE As %String(XMLPROJECTION = "ATTRIBUTE");
}

Also consider the following XML file:

<?xml version="1.0" encoding="UTF-8"?>
<Test PropertyD="">
  <PropertyA></PropertyA>
  <PropertyB xsi:nil="true" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</Test>

If you create an instance of %XML.ReaderOpens in a new tab and use it to import this file into the previous class, you get the following result:

  • PropertyA and PropertyD equal $char(0).

  • All other properties equal "".

For example, if you wrote a routine to examine each property, inspect its value, and write output, you might get something like the following:

PropertyA is $char(0)
PropertyB is null
PropertyC is null
PropertyD is $char(0)
PropertyE is null

Example: IgnoreNull Is 1

In a variation of the preceding example, we set the IgnoreNull property of the reader equal to 1. In this case, all properties equal "".

For example, if you wrote a routine to examine each property, inspect its value, and write output, you might get something like the following:

PropertyA is null
PropertyB is null
PropertyC is null
PropertyD is null
PropertyE is null

Other Useful Methods

On occasion, you may need to use the following additional methods of %XML.ReaderOpens in a new tab:

  • Use the Rewind() method if you need to restart reading from the beginning of the XML source document. This method clears all correlations.

  • Use the Close() method if you want to explicitly close and clean up the import handler. The import handler is cleaned up automatically; this method is included for backward compatibility.

Reader Properties

You can set the following properties of %XML.ReaderOpens in a new tab to control the overall behavior of your method:

  • Use the UsePPGHandler property to specify whether the instance of %XML.ReaderOpens in a new tab uses process-private globals when it parses the document. If this property is true, the instance uses process-private globals. If this property is false, the instance uses memory. If this property is not set (or equals an empty string), the instance uses the default, which is normally memory.

    Use the Format property to specify the overall format of the XML document. Specify one of the following values:

    • "literal", the default, which is used in most of the examples in this topic.

    • "encoded", encoded as described in the SOAP 1.1 standard.

    • "encoded12", encoded as described in the SOAP 1.2 standard.

    Note that you can override the Format property within the OpenFile(), OpenStream(), OpenString(), and OpenURL() methods.

    This property has no effect unless you use Correlate() and Next().

  • Use the Summary property to force the reader to import only the summary fields of the XML-enabled objects. As noted in Projecting Objects to XML, the summary of an object is specified by its XMLSUMMARY class parameter, which you specify as a comma-separated list of properties.

  • Use the IgnoreSAXWarnings property to specify whether the reader should report warnings issued by the SAX parser.

%XML.ReaderOpens in a new tab also provides properties that you can use to examine the document you are reading:

  • The Document property contains an instance of %XML.DocumentOpens in a new tab that represents the entire, parsed document that you are reading. For information on %XML.DocumentOpens in a new tab, see the class reference.

  • The Node property is a string that represents the current node of the XML document. Note that 0 means the document, that is, the parent of the root element.

For information on the EntityResolver, SAXFlags, and SAXSchemaSpec properties, see Customizing How the SAX Parser Is Used. Note that the SAXMask property is ignored.

Redefining How the Reader Handles Correlated Objects

When an instance of %XML.ReaderOpens in a new tab finds an XML element that is correlated to an XML-enabled class, the reader calls the XMLNew() method of that class, which in turn calls %New() by default. That is, when a reader finds a correlated element, it creates a new object of the correlated class. The new object is populated with the data that you read from the XML document.

You can customize this behavior by redefining XMLNew() in your XML-enabled class (or perhaps in your own custom XML adaptor). For example, this method can instead open an existing instance of that class. The existing instance then receives the data that you read from the XML document.

The following examples show how you can modify XMLNew() to update existing instances with new data from an XML document.

In both examples, for simplicity, we assume that a node in the XML document contains an ID that we can compare to the IDs in the extent for the class. We could of course compare the XML document to the existing objects in other ways.

When %XML.Reader Calls XMLNew()

For reference, %XML.ReaderOpens in a new tab calls the XMLNew() method automatically in two circumstances:

  • %XML.ReaderOpens in a new tab calls XMLNew() when you call the Next() method of %XML.ReaderOpens in a new tab, after you have correlated XML elements (in an external document) with an XML-enabled class. The Next() method gets the next element from the document, calls XMLNew() to create an instance of the appropriate object, and then imports the element into the object.

  • Similarly, %XML.ReaderOpens in a new tab calls XMLNew() for any object-valued properties of the correlated XML elements.

Example 1: Modifying XMLNew() in an XML-Enabled Class

First consider the following XML file:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <Person>
    <IRISID>4</IRISID>
    <Name>Quine,Maria K.</Name>
    <DOB>1964-11-14</DOB>
    <Address>
      <City>Hialeah</City>
      <Zip>94999</Zip>
    </Address>
    <Doctors>
      <Doctor>
        <Name>Vanzetti,Debra B.</Name>
      </Doctor>
    </Doctors>
  </Person>
...

This file maps to the following InterSystems IRIS class (partly shown):

Class GXML.PersonWithXMLNew Extends (%Persistent,%Populate,%XML.Adaptor)
{

Parameter XMLNAME="Person";

/// make sure this is the same as the XMLNAME of the property 
/// in this class that is of type %XML.Id
Parameter NAMEOFEXPORTID As %String = "IRISID";

Property IdForExport 
As %XML.Id(XMLNAME="IRISID",XMLPROJECTION="ELEMENT") [Private,Transient];

Property Name As %Name;

Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h");

Property Address As GXML.Address;

Property Doctors As list Of GXML.Doctor;

}

In this class, the purpose of the IdForExport property is to project the InterSystems IRIS internal ID to an element (IRISID) when objects of this class are exported. (In this particular example, this enables us to easily generate suitable files for import. Your class does not have to contain such a property.)

The NAMEOFEXPORTID parameter is used to indicate the element that we use for the InterSystems IRIS ID when we export objects of this class. This is included only for the convenience of the customized XMLNew() method that we have also added to this class. This method is defined as follows:

ClassMethod XMLNew(doc As %XML.Document, node As %Integer, 
contOref As %RegisteredObject = "") As GXML.PersonWithXMLNew
{
    Set id=""
    Set tmpnode=doc.GetNode(node)
    Do tmpnode.MoveToFirstChild()
    Do {
        //compare data node to the string given by the NAMEOFEXPORTID parameter
        //which indicates the XMLNAME of the ids for this object
        If tmpnode.NodeData=..#NAMEOFEXPORTID {
            //get the text from this node; this corresponds to an id in the database
            Do tmpnode.GetText(.id)}
        } While tmpnode.MoveToNextSibling()
    
    //if there is no id in the given node, create a new object
    If id="" {
        Write !, "Creating a new object..."
        Quit ..%New()}
    
    //open the given object
    Set result=..%OpenId(id)
    
    //if the id doesn't correspond to an existing object, create a new object
    If result=$$$NULLOREF {
        Write !, "Creating a new object..." 
        Quit ..%New()}

    Write !, "Updating an existing object..."

    Quit result
}

%XML.ReaderOpens in a new tab calls this method when it reads an XML document and correlates a node to GXML.PersonWithXMLNew. This method looks at the value of the NAMEOFEXPORTID parameter in this class, which is IRISID. It then examines the node in the document with the element IRISID and gets its value.

If this ID corresponds to an existing object of this class, the method opens that instance. Otherwise, the method opens a new instance of this class. In both cases, the instance receives the properties as specified in the XML document.

Finally, the following utility class includes a method that opens the XML file and calls %XML.ReaderOpens in a new tab:

Class GXML.DemoXMLNew Extends %RegisteredObject
{

ClassMethod ReadFile(filename As %String = "c:\temp\sample.xml")
{
    Set reader=##class(%XML.Reader).%New()
    Set sc=reader.OpenFile(filename)
    If $$$ISERR(sc) {Do $system.OBJ.DisplayError(sc) Quit  }
    
    Do reader.Correlate("Person","GXML.PersonWithXMLNew")
    
    //loop through elements in file 
    While reader.Next(.person,.sc) {
        Write !,person.Name,!
        Set sc=person.%Save()
        If $$$ISERR(sc) {Do $system.OBJ.DisplayError(sc) Quit  }
    }

    Quit
}

}

When you run the preceding method, one of the following occurs for each <Person> element in the file:

  • An existing object is opened, updated with details from the file, and saved.

  • Or a new object is created, with details from the file.

For example:

GXML>d ##class(GXML.DemoXMLNew).ReadFile()
 
Updating an existing object...
Zampitello,Howard I.
 
Creating a new object...
Smyth,Linda D.
 
Creating a new object...
Vanzetti,Howard I.
 

Example 2: Modifying XMLNew() in a Custom XML Adaptor

In the second example, we create a custom XML adaptor to perform the same actions as shown in the first example. The adaptor class is as follows:

Class GXML.AdaptorWithXMLNew Extends %XML.Adaptor
{

/// make sure this is the same as the XMLNAME of the property in this class
/// that is of type %XML.Id
Parameter NAMEOFEXPORTID As %String = "IRISID";

Property IdForExport 
As %XML.Id(XMLNAME="IRISID",XMLPROJECTION="ELEMENT") [Private,Transient];

ClassMethod XMLNew(document As %XML.Document, node As %Integer, 
containerOref As %RegisteredObject = "") As %RegisteredObject 
[CodeMode=objectgenerator,GenerateAfter=%XMLGenerate,ServerOnly=1]
{
 If %compiledclass.Name'="GXML.AdaptorWithXMLNew" {
  Do %code.WriteLine(" Set id=""""")
  Do %code.WriteLine(" Set tmpnode=document.GetNode(node)")
  Do %code.WriteLine(" Do tmpnode.MoveToFirstChild()")
  Do %code.WriteLine(" Do {")
  Do %code.WriteLine("     If tmpnode.NodeData=..#NAMEOFEXPORTID ")
  Do %code.WriteLine("       {Do tmpnode.GetText(.id)}")
  Do %code.WriteLine(" } While tmpnode.MoveToNextSibling() ")
  Do %code.WriteLine(" If id="""" {")
  Do %code.WriteLine("  Write !,""Creating new object...""")
  Do %code.WriteLine("  Quit ##class("_%class.Name_").%New()}")
  Do %code.WriteLine(" set result=##class("_%class.Name_").%OpenId(id)")
  Do %code.WriteLine(" If result=$$$NULLOREF {")
  Do %code.WriteLine("     Write !,""Creating new object...""")
  Do %code.WriteLine("     Quit ##class("_%class.Name_").%New() }")
  Do %code.WriteLine(" Write !,""Updating existing object ...""")
  Do %code.WriteLine(" Quit result")

  }

 QUIT $$$OK
}

}

The IdForExport property and the NAMEOFEXPORTID parameter establish a convention for how we project the InterSystems IRIS internal ID to an element when objects of subclasses are exported. The intent is that if you redefine IdForExport in a subclass, you redefine NAMEOFEXPORTID correspondingly.

In this class, the XMLNew() method is a method generator. When this class (or any subclass) is compiled, InterSystems IRIS writes the code shown here into the body of this method. See Method Generators in Defining and Using Classes.

The following class extends our custom adaptor:

Class GXML.PersonWithXMLNew2 
Extends (%Persistent, %Populate, GXML.AdaptorWithXMLNew)
{

Parameter XMLNAME = "Person";

Property Name As %Name;

Property DOB As %Date(FORMAT = 5, MAXVAL = "+$h");

Property Address As GXML.Address;

Property Doctors As list Of GXML.Doctor;

}

When you run the sample ReadFile method shown earlier, for each <Person> element in the file, the method either creates and saves a new record or opens and updates an existing record.

Additional Examples

This section contains some additional examples.

Flexible Reader Class

The following example shows a more flexible reader method that accepts the filename, directory, class, and element as input arguments. The method saves each object that it reads.

Class Readers.BasicReader Extends %RegisteredObject
{

ClassMethod Read(mydir, myfile, class, element)
{
   set reader=##class(%XML.Reader).%New()
   if $extract(mydir,$length(mydir))'="/" {set mydir=mydir_"/"}
   set file=mydir_myfile
   set status=reader.OpenFile(file)
   if $$$ISERR(status) {do $System.Status.DisplayError(status)}

   do reader.Correlate(element,class)

   while reader.Next(.object,.status)
   {
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      set status=object.%Save()
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      }
}

}

Notice that when you read in a Person object, you automatically read in its corresponding Address object. (And when you save the Person object, you automatically save the corresponding Address object as well.) This is a fairly crude technique that would be suitable only for bulk load of data; it does not make any attempt to compare to or update existing data.

In order to use this method, you would need an XML-enabled class whose projection matched the incoming XML document. Suppose that you had XML-enabled classes named MyApp.PersonWithAddress and MyApp.Address. Also suppose that you had an XML document as follows:

<?xml version="1.0" encoding="UTF-8"?>
<Root>
   <Person>
      <Name>Able, Andrew</Name>
      <DOB>1977-10-06</DOB>
      <Address>
         <Street>6218 Clinton Drive</Street>
         <City>Reston</City>
         <State>TN</State>
         <Zip>87639</Zip>
      </Address>
   </Person>
</Root>

To read the objects in this file and save them to disk, you would do something like the following:

 set dir="C:\XMLread-these"
 set file="PersonData.txt"
 set cls="MyApp.PersonWithAddress"
 set element="Person"
 do ##class(Readers.BasicReader).Read(dir,file,cls,element)

Reading a String

The following method accepts an XML string, class, and element as input arguments. It saves each object that it reads.

Class Readers.BasicReader Extends %RegisteredObject
{

ClassMethod ReadString(string, class, element)
{
   set reader=##class(%XML.Reader).%New()
   set status=reader.OpenString(string)
   if $$$ISERR(status) {do $System.Status.DisplayError(status)}

   do reader.Correlate(element,class)

   while reader.Next(.object,.status)
   {
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      set status=object.%Save()
      if $$$ISERR(status) {do $System.Status.DisplayError(status)}
      }
}

}

To use this method, you would do something like the following:

 set cls="MyApp.Person"
 set element="Person"
 do ##class(Readers.BasicReader).ReadString(string,cls,element)
FeedbackOpens in a new tab