Repository: microsoftgraph/msgraph-sdk-java
Issue: Provide way to convert returned objects of derived types

Some calls to Graph return a variety of derived objects. For example, a call to `me/messages/id/attachments/id` can return either a `fileAttachment` or an `itemAttachment`. Currently, there is no way to access the unique properties on these derived types because the library only returns the base type. See more in the thread @davidmoten started in #21.

Changed files:
src/test/java/com/microsoft/graph/serializer/DefaultSeralizerTests.java, src/main/java/com/microsoft/graph/serializer/DefaultSerializer.java

diff --git a/src/main/java/com/microsoft/graph/serializer/DefaultSerializer.java b/src/main/java/com/microsoft/graph/serializer/DefaultSerializer.java
index 8b8999d66b..71ab06f0f4 100644
--- a/src/main/java/com/microsoft/graph/serializer/DefaultSerializer.java
+++ b/src/main/java/com/microsoft/graph/serializer/DefaultSerializer.java
@@ -22,6 +22,7 @@
 
 package com.microsoft.graph.serializer;
 
+import com.google.common.base.CaseFormat;
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
@@ -58,19 +59,26 @@ public class DefaultSerializer implements ISerializer {
      * Deserialize an object from the input string.
      *
      * @param inputString The string that stores the representation of the item.
-     * @param clazz       The .class of the item to be deserialized.
+     * @param clazz       The class of the item to be deserialized.
      * @param <T>         The type of the item to be deserialized.
      * @return The deserialized item from the input string.
      */
     @Override
     public <T> T deserializeObject(final String inputString, final Class<T> clazz) {
-        final T jsonObject = gson.fromJson(inputString, clazz);
+        T jsonObject = gson.fromJson(inputString, clazz);
 
-        // Populate the json backed fields for any annotations that are not in the object model
+        // Populate the JSON-backed fields for any annotations that are not in the object model
         if (jsonObject instanceof IJsonBackedObject) {
             logger.logDebug("Deserializing type " + clazz.getSimpleName());
             final IJsonBackedObject jsonBackedObject = (IJsonBackedObject) jsonObject;
             final JsonObject rawObject = gson.fromJson(inputString, JsonObject.class);
+            
+        	// If there is a derived class, try to get it and deserialize to it
+			Class derivedClass = this.getDerivedClass(rawObject, clazz);
+			if (derivedClass != null) {
+				jsonObject = (T) gson.fromJson(inputString, derivedClass);
+			}
+			
             jsonBackedObject.setRawObject(this, rawObject);
             jsonBackedObject.additionalDataManager().setAdditionalData(rawObject);
         } else {
@@ -116,4 +124,38 @@ public class DefaultSerializer implements ISerializer {
     private boolean fieldIsOdataTransient(Map.Entry<String, JsonElement> entry) {
         return entry.getKey().startsWith("@");
     }
+    
+    /**
+     * Get the derived class for the given JSON object
+     * This covers scenarios in which the service may return one of several derived types
+     * of a base object, which it defines using the odata.type parameter
+     * @param jsonObject The raw JSON object of the response
+     * @param parentClass The parent class the derived class should inherit from
+     * @return The derived class if found, or null if not applicable
+     */
+    private Class getDerivedClass(JsonObject jsonObject, Class parentClass) {
+    	//Identify the odata.type information if provided
+        if (jsonObject.get("@odata.type") != null) {
+        	String odataType = jsonObject.get("@odata.type").getAsString();
+        	String derivedType = odataType.substring(odataType.lastIndexOf('.') + 1); //Remove microsoft.graph prefix
+        	derivedType = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, derivedType);
+        	derivedType = "com.microsoft.graph.models.extensions." + derivedType; //Add full package path
+        	
+        	try {
+        		Class derivedClass = Class.forName(derivedType);
+        		//Check that the derived class inherits from the given parent class
+        		if (parentClass.isAssignableFrom(derivedClass)) {
+        			return derivedClass;
+        		}
+        		return null;
+        	} catch (ClassNotFoundException e) {
+        		logger.logDebug("Unable to find a corresponding class for derived type " + derivedType + ". Falling back to parent class.");
+        		//If we cannot determine the derived type to cast to, return null
+        		//This may happen if the API and the SDK are out of sync
+        		return null;
+        	}
+        }
+        //If there is no defined OData type, return null
+        return null;
+    }
 }
diff --git a/src/test/java/com/microsoft/graph/serializer/DefaultSeralizerTests.java b/src/test/java/com/microsoft/graph/serializer/DefaultSeralizerTests.java
index e8550ca50b..29aac1edc0 100644
--- a/src/test/java/com/microsoft/graph/serializer/DefaultSeralizerTests.java
+++ b/src/test/java/com/microsoft/graph/serializer/DefaultSeralizerTests.java
@@ -7,9 +7,11 @@ import org.junit.Before;
 import org.junit.Test;
 
 import com.microsoft.graph.models.extensions.Drive;
+import com.microsoft.graph.models.extensions.FileAttachment;
 import com.microsoft.graph.models.generated.RecurrenceRangeType;
 import com.microsoft.graph.models.generated.BaseRecurrenceRange;
 import com.microsoft.graph.logger.DefaultLogger;
+import com.microsoft.graph.models.extensions.Attachment;
 import com.microsoft.graph.models.extensions.DateOnly;
 
 public class DefaultSeralizerTests {
@@ -64,5 +66,16 @@ public class DefaultSeralizerTests {
         assertNotNull(jsonOut);
         assertEquals(expected, jsonOut);
     }
-
+	
+	@Test
+	public void testDeserializeDerivedType() throws Exception {
+		final DefaultSerializer serializer = new DefaultSerializer(new DefaultLogger());
+		String source = "{\\"@odata.context\\": \\"/attachments/$entity\\",\\"@odata.type\\": \\"#microsoft.graph.fileAttachment\\",\\"id\\": \\"AAMkAGQ0MjBmNWVkLTYxZjUtNDRmYi05Y2NiLTBlYjIwNzJjNmM1NgBGAAAAAAC6ff7latYeQqu_gLrhSAIhBwCF7iGjpaOmRqVwbZc-xXzwAAAAAAEMAACF7iGjpaOmRqVwbZc-xXzwAABQStA0AAABEgAQAFbGmeisbjtLnQdp7kC_9Fk=\\",\\"lastModifiedDateTime\\": \\"2018-01-23T21:50:22Z\\",\\"name\\": \\"Test Book.xlsx\\",\\"contentType\\": \\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\\",\\"size\\": 8457,\\"isInline\\": false,\\"contentId\\": null,\\"contentLocation\\": null,\\"contentBytes\\": \\"bytedata\\"}";
+		Attachment result = serializer.deserializeObject(source, Attachment.class);
+		
+		assert(result instanceof FileAttachment);
+		
+		FileAttachment fileAttachment = (FileAttachment) result;
+		assertNotNull(fileAttachment.contentBytes);
+	}
 }