diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
index 07e0e7319f4d5e9e914651594ee72beb380eca68..0f7ce689406394341b9ee3087c2b6c07c7d4d817 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -23,6 +23,7 @@ import org.xml.sax.SAXException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.List;
 
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -33,7 +34,7 @@ import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 
-public class AndroidSafetyLabel {
+public class AndroidSafetyLabel implements AslMarshallable {
 
     public enum Format {
         NULL, HUMAN_READABLE, ON_DEVICE;
@@ -45,31 +46,55 @@ public class AndroidSafetyLabel {
         return mSafetyLabels;
     }
 
-    private AndroidSafetyLabel(SafetyLabels safetyLabels) {
+    public AndroidSafetyLabel(SafetyLabels safetyLabels) {
         this.mSafetyLabels = safetyLabels;
     }
 
     /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
-    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    // TODO(b/329902686): Support parsing from on-device.
     public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
             throws IOException, ParserConfigurationException, SAXException {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         factory.setNamespaceAware(true);
         Document document = factory.newDocumentBuilder().parse(in);
 
-        Element appMetadataBundles =
-                XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
-
-        return AndroidSafetyLabel.createFromHrElement(appMetadataBundles);
+        switch (format) {
+            case HUMAN_READABLE:
+                Element appMetadataBundles =
+                        XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+
+                return new AndroidSafetyLabelFactory()
+                        .createFromHrElements(
+                                XmlUtils.asElementList(
+                                        document.getElementsByTagName(
+                                                XmlUtils.HR_TAG_APP_METADATA_BUNDLES)));
+            case ON_DEVICE:
+                throw new IllegalArgumentException(
+                        "Parsing from on-device format is not supported at this time.");
+            default:
+                throw new IllegalStateException("Unrecognized input format.");
+        }
     }
 
     /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */
-    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    // TODO(b/329902686): Support outputting human-readable format.
     public void writeToStream(OutputStream out, Format format)
             throws IOException, ParserConfigurationException, TransformerException {
         var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
         var document = docBuilder.newDocument();
-        document.appendChild(this.toOdDomElement(document));
+
+        switch (format) {
+            case HUMAN_READABLE:
+                throw new IllegalArgumentException(
+                        "Outputting human-readable format is not supported at this time.");
+            case ON_DEVICE:
+                for (var child : this.toOdDomElements(document)) {
+                    document.appendChild(child);
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unrecognized input format.");
+        }
 
         TransformerFactory transformerFactory = TransformerFactory.newInstance();
         Transformer transformer = transformerFactory.newTransformer();
@@ -81,19 +106,12 @@ public class AndroidSafetyLabel {
         transformer.transform(domSource, streamResult);
     }
 
-    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
-    public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) {
-        Element safetyLabelsEle =
-                XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
-        SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle);
-        return new AndroidSafetyLabel(safetyLabels);
-    }
-
     /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
-        aslEle.appendChild(mSafetyLabels.toOdDomElement(doc));
-        return aslEle;
+        XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc));
+        return List.of(aslEle);
     }
 
     public static void test() {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b0f05b0c633efb448dd75c9c38e9d22a10ef3d8
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> {
+
+    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
+    @Override
+    public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles) {
+        Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles);
+        Element safetyLabelsEle =
+                XmlUtils.getSingleChildElement(
+                        appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
+        SafetyLabels safetyLabels =
+                new SafetyLabelsFactory().createFromHrElements(List.of(safetyLabelsEle));
+        return new AndroidSafetyLabel(safetyLabels);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e64ab0c53c12b18af4047f94d8a59e52239af4c
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallable {
+
+    /** Creates the on-device DOM element from the AslMarshallable Java Object. */
+    List<Element> toOdDomElements(Document doc);
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..b607353791ff10f917acc2c37aab109fa06df053
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallableFactory<T extends AslMarshallable> {
+
+    /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */
+    T createFromHrElements(List<Element> elements);
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
index efdaa4062bdbbf5b39b505afdbd2af8498faf0a8..e5ed63b74ebfbdcb2b2f29899985c899bf697e8c 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
@@ -16,6 +16,10 @@
 
 package com.android.asllib;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -23,21 +27,32 @@ import java.util.Map;
  * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
  * DataType}, which are mapped in {@link DataTypeConstants}
  */
-public class DataCategory {
+public class DataCategory implements AslMarshallable {
+    private final String mCategoryName;
     private final Map<String, DataType> mDataTypes;
 
-    private DataCategory(Map<String, DataType> dataTypes) {
+    public DataCategory(String categoryName, Map<String, DataType> dataTypes) {
+        this.mCategoryName = categoryName;
         this.mDataTypes = dataTypes;
     }
 
+    public String getCategoryName() {
+        return mCategoryName;
+    }
+
     /** Return the type {@link Map} of String type key to {@link DataType} */
 
     public Map<String, DataType> getDataTypes() {
         return mDataTypes;
     }
 
-    /** Creates a {@link DataCategory} given map of {@param dataTypes}. */
-    public static DataCategory create(Map<String, DataType> dataTypes) {
-        return new DataCategory(dataTypes);
+    /** Creates on-device DOM element(s) from the {@link DataCategory}. */
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName());
+        for (DataType dataType : mDataTypes.values()) {
+            XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
+        }
+        return List.of(dataCategoryEle);
     }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a52591eaf8c0a1447f1d24a187a892804340ec0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> {
+    @Override
+    public DataCategory createFromHrElements(List<Element> elements) {
+        String categoryName = null;
+        Map<String, DataType> dataTypeMap = new HashMap<String, DataType>();
+        for (Element ele : elements) {
+            categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+            dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromHrElements(List.of(ele)));
+        }
+
+        return new DataCategory(categoryName, dataTypeMap);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
index d2c3d75b1d9c0df4cf726237ca25e907ca3f10f5..d2fffc0a36f6c60a61b51e662782d321759603ce 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
@@ -18,16 +18,15 @@ package com.android.asllib;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
 
-import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * Data label representation with data shared and data collected maps containing zero or more {@link
  * DataCategory}
  */
-public class DataLabels {
+public class DataLabels implements AslMarshallable {
     private final Map<String, DataCategory> mDataAccessed;
     private final Map<String, DataCategory> mDataCollected;
     private final Map<String, DataCategory> mDataShared;
@@ -65,46 +64,9 @@ public class DataLabels {
         return mDataShared;
     }
 
-    /** Creates a {@link DataLabels} from the human-readable DOM element. */
-    public static DataLabels createFromHrElement(Element ele) {
-        Map<String, DataCategory> dataAccessed =
-                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
-        Map<String, DataCategory> dataCollected =
-                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
-        Map<String, DataCategory> dataShared =
-                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
-        return new DataLabels(dataAccessed, dataCollected, dataShared);
-    }
-
-    private static Map<String, DataCategory> getDataCategoriesWithTag(
-            Element dataLabelsEle, String dataCategoryUsageTypeTag) {
-        Map<String, Map<String, DataType>> dataTypeMap =
-                new HashMap<String, Map<String, DataType>>();
-        NodeList dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
-
-        for (int i = 0; i < dataSharedNodeList.getLength(); i++) {
-            Element dataSharedEle = (Element) dataSharedNodeList.item(i);
-            String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
-            String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
-
-            if (!dataTypeMap.containsKey((dataCategoryName))) {
-                dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>());
-            }
-            dataTypeMap
-                    .get(dataCategoryName)
-                    .put(dataTypeName, DataType.createFromHrElement(dataSharedEle));
-        }
-
-        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
-        for (String dataCategoryName : dataTypeMap.keySet()) {
-            Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName);
-            dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes));
-        }
-        return dataCategoryMap;
-    }
-
     /** Gets the on-device DOM element for the {@link DataLabels}. */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element dataLabelsEle =
                 XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
 
@@ -112,7 +74,7 @@ public class DataLabels {
         maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
         maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
 
-        return dataLabelsEle;
+        return List.of(dataLabelsEle);
     }
 
     private void maybeAppendDataUsages(
@@ -130,47 +92,10 @@ public class DataLabels {
             DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
             for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
                 DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
-                Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName);
-                if (!dataType.getPurposeSet().isEmpty()) {
-                    Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
-                    purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
-                    purposesEle.setAttribute(
-                            XmlUtils.OD_ATTR_NUM, String.valueOf(dataType.getPurposeSet().size()));
-                    for (DataType.Purpose purpose : dataType.getPurposeSet()) {
-                        Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
-                        purposeEle.setAttribute(
-                                XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
-                        purposesEle.appendChild(purposeEle);
-                    }
-                    dataTypeEle.appendChild(purposesEle);
-                }
-
-                maybeAddBoolToOdElement(
-                        doc,
-                        dataTypeEle,
-                        dataType.getIsCollectionOptional(),
-                        XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
-                maybeAddBoolToOdElement(
-                        doc,
-                        dataTypeEle,
-                        dataType.getIsSharingOptional(),
-                        XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
-                maybeAddBoolToOdElement(
-                        doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
-
-                dataCategoryEle.appendChild(dataTypeEle);
+                XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
             }
             dataUsageEle.appendChild(dataCategoryEle);
         }
         dataLabelsEle.appendChild(dataUsageEle);
     }
-
-    private static void maybeAddBoolToOdElement(
-            Document doc, Element parentEle, Boolean b, String odName) {
-        if (b == null) {
-            return;
-        }
-        Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
-        parentEle.appendChild(ele);
-    }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..c758ab923bbfad88e4faef68adfff41a2352e854
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> {
+
+    /** Creates a {@link DataLabels} from the human-readable DOM element. */
+    @Override
+    public DataLabels createFromHrElements(List<Element> elements) {
+        Element ele = XmlUtils.getSingleElement(elements);
+        Map<String, DataCategory> dataAccessed =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
+        Map<String, DataCategory> dataCollected =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
+        Map<String, DataCategory> dataShared =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
+        return new DataLabels(dataAccessed, dataCollected, dataShared);
+    }
+
+    private static Map<String, DataCategory> getDataCategoriesWithTag(
+            Element dataLabelsEle, String dataCategoryUsageTypeTag) {
+        Map<String, Map<String, DataType>> dataTypeMap =
+                new HashMap<String, Map<String, DataType>>();
+        NodeList dataUsedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
+        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
+
+        Set<String> dataCategoryNames = new HashSet<String>();
+        for (int i = 0; i < dataUsedNodeList.getLength(); i++) {
+            Element dataUsedEle = (Element) dataUsedNodeList.item(i);
+            String dataCategoryName = dataUsedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            dataCategoryNames.add(dataCategoryName);
+        }
+        for (String dataCategoryName : dataCategoryNames) {
+            var dataCategoryElements =
+                    XmlUtils.asElementList(dataUsedNodeList).stream()
+                            .filter(
+                                    ele ->
+                                            ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY)
+                                                    .equals(dataCategoryName))
+                            .toList();
+            DataCategory dataCategory =
+                    new DataCategoryFactory().createFromHrElements(dataCategoryElements);
+            dataCategoryMap.put(dataCategoryName, dataCategory);
+        }
+        return dataCategoryMap;
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
index 7451c69231136fa61ffeb6c1d7e7f2c2ac0d5ce1..5ba29757e19e07cc345a02b12144a301f34b08c3 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
@@ -16,17 +16,18 @@
 
 package com.android.asllib;
 
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
-import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * Data usage type representation. Types are specific to a {@link DataCategory} and contains
  * metadata related to the data usage purpose.
  */
-public class DataType {
+public class DataType implements AslMarshallable {
+
     public enum Purpose {
         PURPOSE_APP_FUNCTIONALITY(1),
         PURPOSE_ANALYTICS(2),
@@ -78,22 +79,30 @@ public class DataType {
         }
     }
 
+    private final String mDataTypeName;
+
     private final Set<Purpose> mPurposeSet;
     private final Boolean mIsCollectionOptional;
     private final Boolean mIsSharingOptional;
     private final Boolean mEphemeral;
 
-    private DataType(
+    public DataType(
+            String dataTypeName,
             Set<Purpose> purposeSet,
             Boolean isCollectionOptional,
             Boolean isSharingOptional,
             Boolean ephemeral) {
+        this.mDataTypeName = dataTypeName;
         this.mPurposeSet = purposeSet;
         this.mIsCollectionOptional = isCollectionOptional;
         this.mIsSharingOptional = isSharingOptional;
         this.mEphemeral = ephemeral;
     }
 
+    public String getDataTypeName() {
+        return mDataTypeName;
+    }
+
     /**
      * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
      * and type
@@ -126,20 +135,42 @@ public class DataType {
         return mEphemeral;
     }
 
-    /** Creates a {@link DataType} from the human-readable DOM element. */
-    public static DataType createFromHrElement(Element hrDataTypeEle) {
-        Set<Purpose> purposeSet =
-                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
-                        .map(Purpose::forString)
-                        .collect(Collectors.toUnmodifiableSet());
-        Boolean isCollectionOptional =
-                XmlUtils.fromString(
-                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
-        Boolean isSharingOptional =
-                XmlUtils.fromString(
-                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
-        Boolean ephemeral =
-                XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
-        return new DataType(purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName());
+        if (!this.getPurposeSet().isEmpty()) {
+            Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
+            purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
+            purposesEle.setAttribute(
+                    XmlUtils.OD_ATTR_NUM, String.valueOf(this.getPurposeSet().size()));
+            for (DataType.Purpose purpose : this.getPurposeSet()) {
+                Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
+                purposeEle.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
+                purposesEle.appendChild(purposeEle);
+            }
+            dataTypeEle.appendChild(purposesEle);
+        }
+
+        maybeAddBoolToOdElement(
+                doc,
+                dataTypeEle,
+                this.getIsCollectionOptional(),
+                XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
+        maybeAddBoolToOdElement(
+                doc,
+                dataTypeEle,
+                this.getIsSharingOptional(),
+                XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
+        maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
+        return List.of(dataTypeEle);
+    }
+
+    private static void maybeAddBoolToOdElement(
+            Document doc, Element parentEle, Boolean b, String odName) {
+        if (b == null) {
+            return;
+        }
+        Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
+        parentEle.appendChild(ele);
     }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..99f8a8b1b1521190a9e7b6ff875353ddc0a7bd44
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DataTypeFactory implements AslMarshallableFactory<DataType> {
+    /** Creates a {@link DataType} from the human-readable DOM element. */
+    @Override
+    public DataType createFromHrElements(List<Element> elements) {
+        Element hrDataTypeEle = XmlUtils.getSingleElement(elements);
+        String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+        Set<DataType.Purpose> purposeSet =
+                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+                        .map(DataType.Purpose::forString)
+                        .collect(Collectors.toUnmodifiableSet());
+        Boolean isCollectionOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
+        Boolean isSharingOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
+        Boolean ephemeral =
+                XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
+        return new DataType(
+                dataTypeName, purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
index 6ba15e1ec4db45cdc0c27cf1705284b8c57c0d0c..f06522fc2a5cdc7332ae951cf4d9e4abdc5f4792 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
@@ -19,13 +19,15 @@ package com.android.asllib;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import java.util.List;
+
 /** Safety Label representation containing zero or more {@link DataCategory} for data shared */
-public class SafetyLabels {
+public class SafetyLabels implements AslMarshallable {
 
     private final Long mVersion;
     private final DataLabels mDataLabels;
 
-    private SafetyLabels(Long version, DataLabels dataLabels) {
+    public SafetyLabels(Long version, DataLabels dataLabels) {
         this.mVersion = version;
         this.mDataLabels = dataLabels;
     }
@@ -40,26 +42,12 @@ public class SafetyLabels {
         return mVersion;
     }
 
-    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
-    public static SafetyLabels createFromHrElement(Element safetyLabelsEle) {
-        Long version;
-        try {
-            version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
-        } catch (Exception e) {
-            throw new IllegalArgumentException(
-                    "Malformed or missing required version in safety labels.");
-        }
-        Element dataLabelsEle =
-                XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS);
-        DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle);
-        return new SafetyLabels(version, dataLabels);
-    }
-
     /** Creates an on-device DOM element from the {@link SafetyLabels}. */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element safetyLabelsEle =
                 XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
-        safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc));
-        return safetyLabelsEle;
+        XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc));
+        return List.of(safetyLabelsEle);
     }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..68e83fe30db1c024c1cf7cd683ce3885d32a1872
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> {
+
+    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
+    @Override
+    public SafetyLabels createFromHrElements(List<Element> elements) {
+        Element safetyLabelsEle = XmlUtils.getSingleElement(elements);
+        Long version;
+        try {
+            version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Malformed or missing required version in safety labels.");
+        }
+
+        DataLabels dataLabels =
+                new DataLabelsFactory()
+                        .createFromHrElements(
+                                List.of(
+                                        XmlUtils.getSingleChildElement(
+                                                safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS)));
+        return new SafetyLabels(version, dataLabels);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
index 4392c2c220c4814d53922ac6c456757e7399b27a..3c89a308036f5ccf186aa16826f7eb49d7e0827d 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -20,6 +20,9 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class XmlUtils {
     public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
     public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
@@ -60,30 +63,60 @@ public class XmlUtils {
     /** Gets the single top-level {@link Element} having the {@param tagName}. */
     public static Element getSingleElement(Document doc, String tagName) {
         var elements = doc.getElementsByTagName(tagName);
-        return getSingleElement(elements, tagName);
+        return getSingleElement(elements);
     }
 
     /**
      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
      */
-    public static Element getSingleElement(Element parentEle, String tagName) {
+    public static Element getSingleChildElement(Element parentEle, String tagName) {
         var elements = parentEle.getElementsByTagName(tagName);
-        return getSingleElement(elements, tagName);
+        return getSingleElement(elements);
     }
 
-    /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */
-    public static Element getSingleElement(NodeList elements, String tagName) {
+    /** Gets the single {@link Element} from {@param elements} */
+    public static Element getSingleElement(NodeList elements) {
         if (elements.getLength() != 1) {
             throw new IllegalArgumentException(
-                    String.format("Expected 1 %s but got %s.", tagName, elements.getLength()));
+                    String.format(
+                            "Expected 1 element in NodeList but got %s.", elements.getLength()));
         }
         var elementAsNode = elements.item(0);
         if (!(elementAsNode instanceof Element)) {
-            throw new IllegalStateException(String.format("%s was not an element.", tagName));
+            throw new IllegalStateException(
+                    String.format("%s was not an element.", elementAsNode.getNodeName()));
         }
         return ((Element) elementAsNode);
     }
 
+    /** Gets the single {@link Element} within {@param elements}. */
+    public static Element getSingleElement(List<Element> elements) {
+        if (elements.size() != 1) {
+            throw new IllegalStateException(
+                    String.format("Expected 1 element in list but got %s.", elements.size()));
+        }
+        return elements.get(0);
+    }
+
+    /** Converts {@param nodeList} into List of {@link Element}. */
+    public static List<Element> asElementList(NodeList nodeList) {
+        List<Element> elementList = new ArrayList<Element>();
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            var elementAsNode = nodeList.item(0);
+            if (elementAsNode instanceof Element) {
+                elementList.add(((Element) elementAsNode));
+            }
+        }
+        return elementList;
+    }
+
+    /** Appends {@param children} to the {@param ele}. */
+    public static void appendChildren(Element ele, List<Element> children) {
+        for (Element c : children) {
+            ele.appendChild(c);
+        }
+    }
+
     /** Gets the Boolean from the String value. */
     public static Boolean fromString(String s) {
         if (s == null) {