As you found, if you just use the direct QueryResult against the SOAP API and include anything in the fields beyond the ID
you get the following response:
SOQL:
Select Id, Name from ApexClass
System.CalloutException: Web service callout failed: Unable to parse callout response. Apex type not found for element Name
The problem is that the underlying WebServiceCallout.invoke
can't unpack the specific sObject into the generic QueryResults.records
sObject as it doesn't have a generic set of fields like the Partner API does.
E.g. The Name field here is a specific extension element over ens:sObject
.
<records xsi:type="sf:ApexClass">
<sf:Id>01p70000000br8TAAQ</sf:Id>
<sf:Name>ApexClassNameHere</sf:Name> <!-- What to do with this? -->
</records>
It's a pain, but you could carry on with your current approach of breaking out the specific classes that extend ens:sObject (plus the associated *_element to unpack it).
In terms of classes that extend sObject, pretty much everything you can query
falls into that category. Let's look at ApexClass.
<xsd:complexType name="ApexClass">
<xsd:complexContent>
<xsd:extension base="ens:sObject">
<xsd:sequence>
<xsd:element name="ApiVersion" minOccurs="0" type="xsd:double" nillable="true"/>
<xsd:element name="Body" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="BodyCrc" minOccurs="0" type="xsd:double" nillable="true"/>
<xsd:element name="CreatedBy" minOccurs="0" type="ens:User" nillable="true"/>
<xsd:element name="CreatedById" minOccurs="0" type="tns:ID" nillable="true"/>
<xsd:element name="CreatedDate" minOccurs="0" type="xsd:dateTime" nillable="true"/>
<xsd:element name="FullName" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="IsValid" minOccurs="0" type="xsd:boolean" nillable="true"/>
<xsd:element name="LastModifiedBy" minOccurs="0" type="ens:User" nillable="true"/>
<xsd:element name="LastModifiedById" minOccurs="0" type="tns:ID" nillable="true"/>
<xsd:element name="LastModifiedDate" minOccurs="0" type="xsd:dateTime" nillable="true"/>
<xsd:element name="LengthWithoutComments" minOccurs="0" type="xsd:int" nillable="true"/>
<xsd:element name="ManageableState" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="Metadata" minOccurs="0" type="mns:ApexClass" nillable="true"/>
<xsd:element name="Name" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="NamespacePrefix" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="Status" minOccurs="0" type="xsd:string" nillable="true"/>
<xsd:element name="SymbolTable" minOccurs="0" type="tns:SymbolTable" nillable="true"/>
<xsd:element name="SystemModstamp" minOccurs="0" type="xsd:dateTime" nillable="true"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
So to actually use that from Apex you need the the class to deseralize into. Last I checked the built in version of Wsdl2Apex won't generate these classes for you. It needs to be generated with both the fields from the parent complexType and the child fields.
public class ApexClass extends sObject_x {
public String type = 'ApexClass';
private String[] type_att_info = new String[]{'xsi:type'};
public Double ApiVersion;
public String Body;
public Double BodyCrc;
public ToolingAPIWSDL.User_x CreatedBy;
public String CreatedById;
public DateTime CreatedDate;
public String FullName;
public Boolean IsValid;
public ToolingAPIWSDL.User_x LastModifiedBy;
public String LastModifiedById;
public DateTime LastModifiedDate;
public Integer LengthWithoutComments;
public ToolingAPIWSDLMetadata.ApexClass Metadata;
public String Name;
public String NamespacePrefix;
public String Status;
public ToolingAPIWSDL.SymbolTable SymbolTable;
public DateTime SystemModstamp;
public String[] fieldsToNull;
public String Id;
private String[] ApiVersion_type_info = new String[]{'ApiVersion','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] Body_type_info = new String[]{'Body','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] BodyCrc_type_info = new String[]{'BodyCrc','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] CreatedBy_type_info = new String[]{'CreatedBy','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] CreatedById_type_info = new String[]{'CreatedById','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] CreatedDate_type_info = new String[]{'CreatedDate','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] FullName_type_info = new String[]{'FullName','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] IsValid_type_info = new String[]{'IsValid','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] LastModifiedBy_type_info = new String[]{'LastModifiedBy','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] LastModifiedById_type_info = new String[]{'LastModifiedById','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] LastModifiedDate_type_info = new String[]{'LastModifiedDate','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] LengthWithoutComments_type_info = new String[]{'LengthWithoutComments','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] Metadata_type_info = new String[]{'Metadata','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] Name_type_info = new String[]{'Name','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] NamespacePrefix_type_info = new String[]{'NamespacePrefix','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] Status_type_info = new String[]{'Status','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] SymbolTable_type_info = new String[]{'SymbolTable','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] SystemModstamp_type_info = new String[]{'SystemModstamp','urn:tooling.soap.sforce.com',null,'0','1','true'};
private String[] fieldsToNull_type_info = new String[]{'fieldsToNull','urn:tooling.soap.sforce.com',null,'0','-1','true'};
private String[] Id_type_info = new String[]{'Id','urn:tooling.soap.sforce.com',null,'1','1','true'};
private String[] apex_schema_type_info = new String[]{'urn:tooling.soap.sforce.com','true','false'};
private String[] field_order_type_info = new String[]{'ApiVersion','Body','BodyCrc','CreatedBy','CreatedById','CreatedDate','FullName','IsValid','LastModifiedBy','LastModifiedById','LastModifiedDate','LengthWithoutComments','Metadata','Name','NamespacePrefix','Status','SymbolTable','SystemModstamp','fieldsToNull','Id'};
}
There are ways to automate the generation of these classes from the WSDL.
One thing you might want to investigate is the apex-toolingapi repo. It has a pretty generic query method implementation returning a QueryResult.
public ToolingAPIWSDL.QueryResult query(String queryString) {
ToolingAPIWSDL.query_element request_x = new ToolingAPIWSDL.query_element();
request_x.queryString = queryString;
ToolingAPIWSDL.queryResponse_element response_x;
Map<String, ToolingAPIWSDL.queryResponse_element> response_map_x = new Map<String, ToolingAPIWSDL.queryResponse_element>();
response_map_x.put('response_x', response_x);
System.debug('JWL: request: ' + request_x);
System.debug('JWL: this endpoint: ' + this.endpoint_x);
WebServiceCallout.invoke(
this,
request_x,
response_map_x,
new String[]{endpoint_x,
'',
'urn:tooling.soap.sforce.com',
'query',
'urn:tooling.soap.sforce.com',
'queryResponse',
'ToolingAPIWSDL.queryResponse_element'}
);
response_x = response_map_x.get('response_x');
return response_x.result;
}
Yet somehow they don't run into the "Apex type not found" issues and can then generically unpack to the required class type.
public ToolingAPIWSDL.QueryResult query(String queryString,String recordClassName) {
ToolingAPIWSDL.QueryResult qr = service.query(queryString);
return castQueryResultToSubclass(qr,recordClassName);
}
How have they performed this miraculously feat of deserialization? Are they wizards?
Spoiler:
They cheated and put all the possible response fields on sObject_x
.
Have a look at lines 178 to 417...