diff --git a/.gitignore b/.gitignore
index 90f0431277669366cb6e9d29c5a4fd09a899abec..222fb6170412423a5f8f5add107be1fc6bf4f21e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 target/**
+/nbproject/
diff --git a/pom.xml b/pom.xml
index dbc0d02dd632f002bc74326a41bd615804e31c23..6a8747ba5cae8d1e01c85d7900a35c5c994d80ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,5 +29,50 @@
             <artifactId>jackson-module-jaxb-annotations</artifactId>
             <version>2.10.3</version>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
+    <build>
+        <plugins>            
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.6</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>report</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
 </project>
\ No newline at end of file
diff --git a/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoDeserializer.java b/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoDeserializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..62ada79cfed0fe7d9d8ee34a90b3f30183929d82
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoDeserializer.java
@@ -0,0 +1,58 @@
+package it.inaf.oats.vospace.datamodel;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import java.io.IOException;
+import java.util.Map;
+import net.ivoa.xml.uws.v1.JobSummary.JobInfo;
+import net.ivoa.xml.vospace.v2.Transfer;
+
+public class JobInfoDeserializer extends StdDeserializer<JobInfo> {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    public JobInfoDeserializer() {
+        super(JobInfo.class);
+    }
+
+    @Override
+    public JobInfo deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
+        Object content = jp.getCodec().readValue(jp, Object.class);
+
+        if (content == null) {
+            return null;
+        }
+
+        if (!(content instanceof Map)) {
+            throw new UnsupportedOperationException("JobInfo contains an instance of " + content.getClass().getCanonicalName());
+        }
+
+        Map<String, Object> map = (Map<String, Object>) content;
+
+        if (map.isEmpty()) {
+            return null;
+        }
+        if (map.keySet().size() > 1) {
+            throw new UnsupportedOperationException("Multiple keys found in JobInfo content");
+        }
+
+        String name = map.keySet().toArray(String[]::new)[0];
+
+        JobInfo jobInfo = new JobInfo();
+
+        switch (name) {
+            case "transfer":
+                String transferJson = MAPPER.writeValueAsString(map.get(name));
+                Transfer transfer = MAPPER.readValue(transferJson, Transfer.class);
+                jobInfo.getAny().add(transfer);
+                break;
+            default:
+                throw new UnsupportedOperationException("JobInfo map key is " + name);
+        }
+
+        return jobInfo;
+    }
+}
diff --git a/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoSerializer.java b/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..e631b29217b86cf8cc52c4ad217e27d3ee19af17
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/datamodel/JobInfoSerializer.java
@@ -0,0 +1,39 @@
+package it.inaf.oats.vospace.datamodel;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import net.ivoa.xml.uws.v1.JobSummary.JobInfo;
+
+public class JobInfoSerializer extends StdSerializer<JobInfo> {
+
+    public JobInfoSerializer() {
+        super(JobInfo.class);
+    }
+
+    @Override
+    public void serialize(JobInfo jobInfo, JsonGenerator jg, SerializerProvider sp) throws IOException {
+
+        List<Object> any = jobInfo.getAny();
+        if (any == null || any.isEmpty()) {
+            jg.getCodec().writeValue(jg, null);
+            return;
+        }
+
+        if (jobInfo.getAny().size() == 1) {
+            Object content = jobInfo.getAny().get(0);
+
+            Map<String, Object> map = new HashMap<>();
+            String name = content.getClass().getSimpleName().toLowerCase();
+            map.put(name, content);
+
+            jg.getCodec().writeValue(jg, map);
+        } else {
+            jg.getCodec().writeValue(jg, jobInfo.getAny());
+        }
+    }
+}
diff --git a/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java b/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java
index f9317641da01fa394bf9a54c24a82ceeedfc918c..1f21455a86d930564380cf5bed31542cecd7d4cb 100644
--- a/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java
+++ b/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java
@@ -8,6 +8,10 @@
 
 package net.ivoa.xml.uws.v1;
 
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import it.inaf.oats.vospace.datamodel.JobInfoDeserializer;
+import it.inaf.oats.vospace.datamodel.JobInfoSerializer;
 import java.util.ArrayList;
 import java.util.List;
 import javax.xml.bind.JAXBElement;
@@ -17,9 +21,12 @@ import javax.xml.bind.annotation.XmlAnyElement;
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementRef;
+import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.datatype.XMLGregorianCalendar;
+import net.ivoa.xml.vospace.v2.Transfer;
 import org.w3c.dom.Element;
 
 
@@ -87,6 +94,8 @@ import org.w3c.dom.Element;
     "errorSummary",
     "jobInfo"
 })
+@XmlSeeAlso({Transfer.class}) // Necessary for setting a Transfer inside the jobInfo property.
+@XmlRootElement(name = "job")
 public class JobSummary {
 
     @XmlElement(required = true)
@@ -501,6 +510,8 @@ public class JobSummary {
     @XmlType(name = "", propOrder = {
         "any"
     })
+    @JsonSerialize(using = JobInfoSerializer.class)
+    @JsonDeserialize(using = JobInfoDeserializer.class)
     public static class JobInfo {
 
         @XmlAnyElement(lax = true)
diff --git a/src/main/java/net/ivoa/xml/uws/v1/package-info.java b/src/main/java/net/ivoa/xml/uws/v1/package-info.java
index 34b50c2169257aa613d75a892af61e1d7f901829..f0848139a7ede580de7a1298e6f58938e4b335b4 100644
--- a/src/main/java/net/ivoa/xml/uws/v1/package-info.java
+++ b/src/main/java/net/ivoa/xml/uws/v1/package-info.java
@@ -5,5 +5,16 @@
 // Generated on: 2020.10.24 at 09:39:16 AM CEST 
 //
 
-@javax.xml.bind.annotation.XmlSchema(namespace = "http://www.ivoa.net/xml/UWS/v1.0", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
+@javax.xml.bind.annotation.XmlSchema(
+        namespace = "http://www.ivoa.net/xml/UWS/v1.0",
+        elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED,
+        // Defining the namespace prefix is necessary otherwise
+        // deserialized XML will have no prefixes
+        xmlns = {
+            @javax.xml.bind.annotation.XmlNs(
+                    namespaceURI = "http://www.ivoa.net/xml/UWS/v1.0",
+                    prefix = "uws"
+            )
+        }
+)
 package net.ivoa.xml.uws.v1;
diff --git a/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java b/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..39f0f97a78d303246db417ac794f6cda4516c530
--- /dev/null
+++ b/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java
@@ -0,0 +1,88 @@
+package net.ivoa.xml.uws.v1;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.StringReader;
+import java.io.StringWriter;
+import javax.xml.bind.JAXB;
+import net.ivoa.xml.uws.v1.JobSummary.JobInfo;
+import net.ivoa.xml.vospace.v2.Protocol;
+import net.ivoa.xml.vospace.v2.Transfer;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class JobSummaryTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Test
+    public void testXmlSerialization() throws Exception {
+
+        JobSummary job = getJobSummary();
+
+        String xml;
+        try ( StringWriter sw = new StringWriter()) {
+            JAXB.marshal(job, sw);
+            xml = sw.toString();
+            System.out.println(xml);
+        }
+
+        assertTrue(xml.contains("<uws:job"));
+        assertTrue(xml.contains("<uws:jobInfo"));
+        assertTrue(xml.contains("<vos:transfer"));
+
+        try ( StringReader sr = new StringReader(xml)) {
+            JobSummary deserialized = JAXB.unmarshal(sr, JobSummary.class);
+            verifyJobsAreEquals(deserialized);
+        }
+    }
+
+    @Test
+    public void testJsonSerialization() throws Exception {
+
+        JobSummary job = getJobSummary();
+
+        String json = MAPPER.writeValueAsString(job);
+        System.out.println(json);
+
+        JobSummary deserialized = MAPPER.readValue(json, JobSummary.class);
+
+        verifyJobsAreEquals(deserialized);
+    }
+
+    private JobSummary getJobSummary() {
+
+        JobSummary job = new JobSummary();
+        job.setJobId("job_id");
+        job.setPhase(ExecutionPhase.PENDING);
+
+        JobInfo jobInfo = new JobInfo();
+
+        Transfer transfer = new Transfer();
+        transfer.setVersion("2.1");
+        transfer.setTarget("vos://example.com!vospace/mydata1");
+        transfer.setDirection("pullFromVoSpace");
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
+        transfer.getProtocol().add(protocol);
+
+        jobInfo.getAny().add(transfer);
+
+        job.setJobInfo(jobInfo);
+
+        return job;
+    }
+
+    private void verifyJobsAreEquals(JobSummary deserializedJob) {
+
+        assertEquals("job_id", deserializedJob.getJobId());
+        assertEquals(ExecutionPhase.PENDING, deserializedJob.getPhase());
+
+        Transfer transfer = (Transfer) deserializedJob.getJobInfo().getAny().get(0);
+        assertEquals("2.1", transfer.getVersion());
+        assertEquals("pullFromVoSpace", transfer.getDirection());
+        assertEquals("vos://example.com!vospace/mydata1", transfer.getTarget());
+
+        Protocol protocol = transfer.getProtocol().get(0);
+        assertEquals("ivo://ivoa.net/vospace/core#httpget", protocol.getUri());
+    }
+}