Error deserializing parameter in WCF service from Android kSOAP
8/5/11: Update near bottom of post
I'm trying to call my WCF web service method from my Android app using kSOAP. The method takes 4 parameters. 3 of them are of type Guid (for which marshalling Java's UUID works), and the last is a custom type: LocationLibrary.Location. This type is in a separate DLL (LocationLibrary) that I load into the WCF web service project and it is basically comprised of two doubles, latitude and longitude.
[OperationContract]
byte[] GetData(Guid deviceId, Guid appId, Guid devKey, LocationLibrary.Location loc);
The Location class in the project LocationLibrary is very simple:
namespace LocationLibrary
{
public class Location
{
public double latitude { get; set; }
public double longitude { get; set; }
public new string ToString()
{
return latitude.ToString() + "," + longitude.ToString();
}
public bool Equals(Location loc)
{
return this.latitude == loc.latitude && this.longitude == loc.longitude;
}
}
}
In my Android project, I've created a class named "Location" that is similar to the .NET version:
public class Location {
public double latitude;
public double longitude;
public Location() {}
public static Location fromString(String s)
{
//Format from SOAP message is "anyType{latitude=39.6572799682617; longitude=-78.9278602600098; }"
Location result = new Location();
String[] tokens = s.split("=");
String lat = tokens[1].split(";")[0];
String lng = tokens[2].split(";")[0];
result.latitude = Double.parseDouble(lat);
result.longitude = Double.parseDouble(lng);
return result;
}
public String toString()
{
return Double.toString(latitude) + "," + Double.toString(longitude);
}
}
When using kSOAP to connect to the web service, I do the following:
private final String SOAP_ACTION = "http://tempuri.org/IMagicSauceV3/GetData";
private final String OPERATION_NAME = "GetData";
private final String WSDL_TARGET_NAMESPACE = "http://tempuri.org/";
private final String SOAP_ADDRESS = "http://mydomain.com/myservice.svc";
private final SoapSerializationEnvelope envelope;
SoapObject request = new SoapObject(WSDL_TARGET_NAMESPACE, OPERATION_NAME);
request.addProperty(Cloud8Connector.deviceIdProperty);
request.addProperty(Cloud8Connector.appIdProperty);
request.addProperty(Cloud8Connector.devKeyProperty);
PropertyInfo locationProperty = new PropertyInfo();
locationProperty.setName("loc");
locationProperty.setValue(Cloud8Connector.myLoc);
locationProperty.setType(Location.class);
request.addProperty(locationProperty);
envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.dotNet = true;
envelope.setOutputSoapObject(request);
MarshalUUID mu = new MarshalUUID();
mu.register(envelope);
MarshalLocation ml = new MarshalLocation();
ml.register(envelope);
byte[] result = null;
HttpTransportSE httpRequest = new HttpTransportSE(SOAP_ADDRESS);
try
{
httpRequest.call(SOAP_ACTION, envelope);
String payloadString = ((SoapPrimitive)(envelope.getResponse())).toString();
result = Base64.decode(payloadString, Base64.DEFAULT);
}
catch(Exception e)
{
e.printStackTrace();
}
As you can see, I've created a simple Marshal class for Location, which basically just uses the fromString and toString methods. I register the envelope with the marshal instance for Location:
MarshalLocation ml = new MarshalLocation();
ml.register(envelope);
Here's the marshal class for Location:
public class MarshalLocation implements Marshal
{
public Object readInstance(XmlPullParser parser, String namespace, String name,
PropertyInfo expected) throws IOException, XmlPullParserException {
return Location.fromString(parser.nextText());
}
public void register(SoapSerializationEnvelope cm) {
cm.addMapping(cm.xsd, "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
writer.text(((Location)obj).toString());
}
}
However, I get this error returned from WCF and cannot seem to get it to work. From the error and from searching, I think I need to tweak something with my web service, but I'm not sure what exactly is the best way to get around this.
I've tried adding this to the web.config file of the web service:
<system.runtime.serialization>
<dataContractSerializer>
<declaredTypes>
<add type="Location, LocationLibrary, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null">
<knownType type="Location, LocationLibrary, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"></knownType>
</add>
</declaredTypes>
</dataContractSerializer>
</system.runtime.serialization>
And I've tried adding the ServiceKnownType attribute to the interface method signature:
[OperationContract]
[ServiceKnownType(typeof(LocationLibrary.Location))]
byte[] GetData(Guid deviceId, Guid appId, Guid devKey, LocationLibrary.Location loc);
But I still get this error :(
Can you point me in the right direction? Thanks!
Error:
07-30 00:42:08.186: WARN/System.err(8723): SoapFault - faultcode: 'a:DeserializationFailed' faultstring: 'The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:loc. The InnerException message was 'Error in line 1 position 522. Element 'http://tempuri.org/:loc' contains data from a type that maps to the name 'http://www.w3.org/2001/XMLSchema:Location'. The deserializer has no knowledge of any type that maps to this name. Consider using a DataContractResolver or add the type corresponding to 'Location' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.' faultactor: 'null' detail: org.kxml2.kdom.Node@4053aa28
Update
Ideally, I wouldn't have to move the LocationLibrary classes inside of the main WCF project since that will complicate handling of legacy clien开发者_运维知识库ts that expect it in a different namespace. However, I did get things to work by moving those classes inside of the main WCF project AND modifying the marshal class in java to this:
public void register(SoapSerializationEnvelope cm) {
cm.addMapping("http://schemas.datacontract.org/2004/07/MagicSauceV3", "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
Location loc = (Location)obj;
writer.startTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "latitude");
writer.text(Double.toString(loc.latitude));
writer.endTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "latitude");
writer.startTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "longitude");
writer.text(Double.toString(loc.longitude));
writer.endTag("http://schemas.datacontract.org/2004/07/MagicSauceV3", "longitude");
}
Now that I got that to work, I just get it working with the LocationLibrary WCF classes how they were (in a separate DLL). So I've modified the marshal class as such:
public void register(SoapSerializationEnvelope cm) {
cm.addMapping("http://schemas.datacontract.org/2004/07/LocationLibrary", "Location", Location.class, this);
}
public void writeInstance(XmlSerializer writer, Object obj) throws IOException {
Location loc = (Location)obj;
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.text(Double.toString(loc.latitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude");
writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
writer.text(Double.toString(loc.longitude));
writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude");
}
This seems like it should succeed since it produces similar XML to the WP7 version that does work.
WP7 working XML request:
<GetData xmlns="http://tempuri.org/">
<deviceId>{valid GUID}</deviceId>
<appId>{valid GUID}</appId>
<devKey>{valid GUID}</devKey>
<loc xmlns:d4p1="http://schemas.datacontract.org/2004/07/LocationLibrary" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:latitude>47.65</d4p1:latitude>
<d4p1:longitude>-122.34</d4p1:longitude>
</loc>
</GetData>
Android non-working XML request:
<GetData xmlns="http://tempuri.org/" id="o0" c:root="1">
<deviceId i:type="d:UUID">{valid GUID}</deviceId>
<appId i:type="d:UUID">{valid GUID}</appId>
<devKey i:type="d:UUID">{valid GUID}</devKey>
<loc i:type="n0:Location" xmlns:n0="http://schemas.datacontract.org/2004/07/LocationLibrary">
<n0:latitude>47.65</n0:latitude>
<n0:longitude>-122.34</n0:longitude>
</loc>
</GetData>
The above Android XML request produces this response error:
08-05 22:51:23.703: WARN/System.err(1382): SoapFault - faultcode: 'a:DeserializationFailed' faultstring: 'The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:loc. The InnerException message was 'Error in line 1 position 590. Element 'http://tempuri.org/:loc' contains data from a type that maps to the name 'http://schemas.datacontract.org/2004/07/LocationLibrary:Location'. The deserializer has no knowledge of any type that maps to this name. Consider using a DataContractResolver or add the type corresponding to 'Location' to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding it to the list of known types passed to DataContractSerializer.'. Please see InnerException for more details.' faultactor: 'null' detail: org.kxml2.kdom.Node@4054bcb8
The only notable difference I see is with the property of "loc" in the working XML request: xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
I don't know if that would make a difference (and it doesn't seem to be referenced with the nested tags). I also don't know how to add that extra namespace without messing up the other one.
Sorry for the long post but I wanted to make sure you have all of the info you need to help :)
Thanks!
Well as I expected you are doing just ToString
but it is wrong. Your location is not transported like string of two concatenated values. It is transported as object serialized to XML. Something like:
<Location> <!-- or loc -->
<latitude>10.0</latitude>
<longitude>-20.0</longitude>
</Location>
Thanks to Ladislav for the point in the right direction! I'm not sure what the etiquette is here to reward that, but I'd be glad to follow if someone would advise.
The answer was two-fold, and I can confirm that it now works with the LocationLibrary's Location class in a separate DLL:
Change how I was forming the XML request in Android using my marshaling class. The writeInstance method was only using the .textMethod of the XmlSerializer class, calling .toString() on the Location class. I changed it to utilize the proper way of forming XML tags (.startTag and .endTag methods):
public void writeInstance(XmlSerializer writer, Object obj) throws IOException { Location loc = (Location)obj; writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude"); writer.text(Double.toString(loc.latitude)); writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "latitude"); writer.startTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude"); writer.text(Double.toString(loc.longitude)); writer.endTag("http://schemas.datacontract.org/2004/07/LocationLibrary", "longitude"); }
Add some simple tags to the WCF service. I added [DataContract] to the Location class in the LocationLibrary assembly utilized by the WCF service, and I added [DataMember] to each of its members:
[DataContract] public class Location { [DataMember] public double latitude { get; set; } [DataMember] public double longitude { get; set; } public new string ToString() { return latitude.ToString() + "," + longitude.ToString(); } public bool Equals(Location loc) { return this.latitude == loc.latitude && this.longitude == loc.longitude; }
精彩评论