diff --git a/tools/fidlcat/lib/library_loader.cc b/tools/fidlcat/lib/library_loader.cc
index 77d66c7bf7f6324fb1ad9c9b8bccc801a22503ff..62de4252c32b03b019b3f9b5f585c2996fab5f3b 100644
--- a/tools/fidlcat/lib/library_loader.cc
+++ b/tools/fidlcat/lib/library_loader.cc
@@ -113,16 +113,17 @@ InterfaceMethod::InterfaceMethod(const Interface& interface,
                                  const rapidjson::Value& value)
     : enclosing_interface_(interface),
       ordinal_(std::strtoll(value["ordinal"].GetString(), nullptr, 10)),
-      name_(value["name"].GetString()), value_(value) {
+      name_(value["name"].GetString()),
+      value_(value) {
   if (value_["has_request"].GetBool()) {
     request_ = std::make_unique<Struct>(interface.enclosing_library(), value,
                                         "maybe_request_size", "maybe_request");
   }
 
   if (value_["has_response"].GetBool()) {
-    response_ = std::make_unique<Struct>(interface.enclosing_library(), value,
-                                         "maybe_response_size",
-                                         "maybe_response");
+    response_ =
+        std::make_unique<Struct>(interface.enclosing_library(), value,
+                                 "maybe_response_size", "maybe_response");
   }
 }
 
@@ -173,6 +174,11 @@ Library::Library(const LibraryLoader& enclosing, rapidjson::Document& document)
                     std::forward_as_tuple(uni["name"].GetString()),
                     std::forward_as_tuple(*this, uni));
   }
+  for (auto& xuni : backing_document_["xunion_declarations"].GetArray()) {
+    xunions_.emplace(std::piecewise_construct,
+                     std::forward_as_tuple(xuni["name"].GetString()),
+                     std::forward_as_tuple(*this, xuni));
+  }
 }
 
 std::unique_ptr<Type> Library::TypeFromIdentifier(
@@ -202,7 +208,11 @@ std::unique_ptr<Type> Library::TypeFromIdentifier(
     }
     return type;
   }
-  // And probably for tables.
+  auto xuni = xunions_.find(identifier);
+  if (xuni != xunions_.end()) {
+    // Note: XUnion and nullable XUnion are encoded in the same way
+    return std::make_unique<XUnionType>(std::ref(xuni->second), is_nullable);
+  }
   return Type::get_illegal();
 }
 
diff --git a/tools/fidlcat/lib/library_loader.h b/tools/fidlcat/lib/library_loader.h
index 3e415c02db4f6f4ea6eed40ebeaa64d9bdec636c..0b8a0d553dc18ae50f167cf0978cd215f3103bdb 100644
--- a/tools/fidlcat/lib/library_loader.h
+++ b/tools/fidlcat/lib/library_loader.h
@@ -59,6 +59,7 @@ class Enum;
 class Struct;
 class Table;
 class Union;
+class XUnion;
 
 class Enum {
  public:
@@ -88,7 +89,7 @@ class Enum {
   const rapidjson::Value& value_;
 };
 
-// TODO: Consider whether this is duplicative of Struct / Table / XUnion member.
+// TODO: Consider whether this is duplicative of Struct / Table member.
 class UnionMember {
  public:
   UnionMember(const Union& enclosing_union, const rapidjson::Value& value)
@@ -110,6 +111,16 @@ class UnionMember {
 
   virtual const Union& enclosing_union() const { return enclosing_union_; }
 
+  // Only valid for xunions.  TODO: clean this up when unions go away - at that
+  // point, there is no value in this method's being optional.
+  std::optional<Ordinal> ordinal() const {
+    if (value_.HasMember("ordinal")) {
+      return std::optional<Ordinal>(
+          std::strtoll(value_["ordinal"].GetString(), nullptr, 10));
+    }
+    return {};
+  }
+
   virtual ~UnionMember() {}
 
  private:
@@ -148,15 +159,28 @@ class Union {
     return std::strtoll(value_["size"].GetString(), nullptr, 10);
   }
 
+ protected:
+  const Library& enclosing_library_;
+  const rapidjson::Value& value_;
+
  private:
   const UnionMember& get_illegal_member() const;
 
-  const Library& enclosing_library_;
-  const rapidjson::Value& value_;
   std::vector<UnionMember> members_;
   mutable std::unique_ptr<UnionMember> illegal_;
 };
 
+class XUnion : public Union {
+  friend class Library;
+
+ public:
+  XUnion(const Library& enclosing_library, const rapidjson::Value& value)
+      : Union(enclosing_library, value) {}
+
+  XUnion(const XUnion& other)
+      : XUnion(other.enclosing_library_, other.value_) {}
+};
+
 class StructMember {
  public:
   StructMember(const Struct& enclosing_struct, const rapidjson::Value& value)
@@ -187,7 +211,8 @@ class Struct {
  public:
   Struct(const Library& enclosing_library, const rapidjson::Value& value,
          std::string size_name, std::string member_name)
-      : enclosing_library_(enclosing_library), value_(value),
+      : enclosing_library_(enclosing_library),
+        value_(value),
         size_(std::strtoll(value_[size_name].GetString(), nullptr, 10)) {
     auto member_arr = value[member_name].GetArray();
     members_.reserve(member_arr.Size());
@@ -309,7 +334,8 @@ class InterfaceMethod {
 class Interface {
  public:
   Interface(const Library& library, const rapidjson::Value& value)
-      : value_(value), enclosing_library_(library),
+      : value_(value),
+        enclosing_library_(library),
         name_(value_["name"].GetString()) {
     for (auto& method : value["methods"].GetArray()) {
       interface_methods_.emplace_back(*this, method);
@@ -388,6 +414,7 @@ class Library {
   std::map<std::string, Struct> structs_;
   std::map<std::string, Table> tables_;
   std::map<std::string, Union> unions_;
+  std::map<std::string, XUnion> xunions_;
 };
 
 // An indexed collection of libraries.
diff --git a/tools/fidlcat/lib/testdata/types.test.fidl b/tools/fidlcat/lib/testdata/types.test.fidl
index 5027ef43700d23ea471f72846dd0553709272ecb..cea23a5efff5c875509bf7ff70849bf43fb21219 100644
--- a/tools/fidlcat/lib/testdata/types.test.fidl
+++ b/tools/fidlcat/lib/testdata/types.test.fidl
@@ -42,6 +42,11 @@ protocol this_is_an_interface {
     NullableUnionIntFirst(int32 i, int_struct_union? isu);
     ShortUnion(u8_u16_union u, int32 i);
 
+    XUnion(int_struct_xunion isu, int32 i);
+    NullableXUnion(int_struct_xunion? isu, int32 i);
+    NullableXUnionIntFirst(int32 i, int_struct_xunion? isu);
+    ShortXUnion(u8_u16_xunion u, int32 i);
+
     Table(value_table table, int32 i);
 };
 
@@ -92,6 +97,16 @@ union u8_u16_union {
     uint16 variant_u16;
 };
 
+xunion int_struct_xunion {
+    int32 variant_i;
+    two_string_struct variant_tss;
+};
+
+xunion u8_u16_xunion {
+    uint8 variant_u8;
+    uint16 variant_u16;
+};
+
 table value_table {
     1: int16 first_int16;
     2: two_string_struct second_struct;
diff --git a/tools/fidlcat/lib/wire_parser_test.cc b/tools/fidlcat/lib/wire_parser_test.cc
index bcf2523795bc1c0e2ca0b688917489d3c4440eeb..48f3d9033b8a3e44e9390e0b9a319f7017181488 100644
--- a/tools/fidlcat/lib/wire_parser_test.cc
+++ b/tools/fidlcat/lib/wire_parser_test.cc
@@ -454,37 +454,39 @@ TEST_WIRE_TO_JSON(NullableStructAndInt, NullableStructAndInt,
 // struct{uint8 f1; uint32 f2;}
 // struct{struct{uint8 f1; uint32 f2;} inner; uint8 f3;}
 
-// Union tests
+// Union and XUnion tests
 
 namespace {
 
-test::fidlcat::examples::int_struct_union GetIntUnion(int32_t i) {
-  test::fidlcat::examples::int_struct_union u;
+using isu = test::fidlcat::examples::int_struct_union;
+using xisu = test::fidlcat::examples::int_struct_xunion;
+
+template <class T>
+T GetIntUnion(int32_t i) {
+  T u;
   u.set_variant_i(i);
   return u;
 }
 
-test::fidlcat::examples::int_struct_union GetStructUnion(std::string v1,
-                                                         std::string v2) {
-  test::fidlcat::examples::int_struct_union u;
+template <class T>
+T GetStructUnion(std::string v1, std::string v2) {
+  T u;
   test::fidlcat::examples::two_string_struct tss =
       TwoStringStructFromVals(v1, v2);
   u.set_variant_tss(tss);
   return u;
 }
 
-std::unique_ptr<test::fidlcat::examples::int_struct_union> GetIntUnionPtr(
-    int32_t i) {
-  std::unique_ptr<test::fidlcat::examples::int_struct_union> ptr(
-      new test::fidlcat::examples::int_struct_union());
+template <class T>
+std::unique_ptr<T> GetIntUnionPtr(int32_t i) {
+  std::unique_ptr<T> ptr(new T());
   ptr->set_variant_i(i);
   return ptr;
 }
 
-std::unique_ptr<test::fidlcat::examples::int_struct_union> GetStructUnionPtr(
-    std::string v1, std::string v2) {
-  std::unique_ptr<test::fidlcat::examples::int_struct_union> ptr(
-      new test::fidlcat::examples::int_struct_union());
+template <class T>
+std::unique_ptr<T> GetStructUnionPtr(std::string v1, std::string v2) {
+  std::unique_ptr<T> ptr(new T());
   test::fidlcat::examples::two_string_struct tss =
       TwoStringStructFromVals(v1, v2);
   ptr->set_variant_tss(tss);
@@ -494,38 +496,72 @@ std::unique_ptr<test::fidlcat::examples::int_struct_union> GetStructUnionPtr(
 }  // namespace
 
 TEST_WIRE_TO_JSON(UnionInt, Union, R"({"isu":{"variant_i":"42"}, "i" : "1"})",
-                  GetIntUnion(42), 1);
+                  GetIntUnion<isu>(42), 1);
+
 TEST_WIRE_TO_JSON(
     UnionStruct, Union,
     R"({"isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}, "i":"1"})",
-    GetStructUnion("harpo", "chico"), 1);
+    GetStructUnion<isu>("harpo", "chico"), 1);
 
 TEST_WIRE_TO_JSON(NullableUnionInt, NullableUnion,
                   R"({"isu":{"variant_i":"42"}, "i" : "1"})",
-                  GetIntUnionPtr(42), 1);
+                  GetIntUnionPtr<isu>(42), 1);
+
 TEST_WIRE_TO_JSON(
     NullableUnionStruct, NullableUnion,
     R"({"isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}, "i":"1"})",
-    GetStructUnionPtr("harpo", "chico"), 1);
+    GetStructUnionPtr<isu>("harpo", "chico"), 1);
 
 TEST_WIRE_TO_JSON(NullableUnionIntFirstInt, NullableUnionIntFirst,
                   R"({"i" : "1", "isu":{"variant_i":"42"}})", 1,
-                  GetIntUnionPtr(42));
+                  GetIntUnionPtr<isu>(42));
+
 TEST_WIRE_TO_JSON(
     NullableUnionIntFirstStruct, NullableUnionIntFirst,
     R"({"i": "1", "isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}})",
-    1, GetStructUnionPtr("harpo", "chico"));
+    1, GetStructUnionPtr<isu>("harpo", "chico"));
+
+TEST_WIRE_TO_JSON(XUnionInt, XUnion, R"({"isu":{"variant_i":"42"}, "i" : "1"})",
+                  GetIntUnion<xisu>(42), 1);
+
+TEST_WIRE_TO_JSON(
+    XUnionStruct, XUnion,
+    R"({"isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}, "i":"1"})",
+    GetStructUnion<xisu>("harpo", "chico"), 1);
+
+TEST_WIRE_TO_JSON(NullableXUnionInt, NullableXUnion,
+                  R"({"isu":{"variant_i":"42"}, "i" : "1"})",
+                  GetIntUnionPtr<xisu>(42), 1);
+
+TEST_WIRE_TO_JSON(
+    NullableXUnionStruct, NullableXUnion,
+    R"({"isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}, "i":"1"})",
+    GetStructUnionPtr<xisu>("harpo", "chico"), 1);
+
+TEST_WIRE_TO_JSON(NullableXUnionIntFirstInt, NullableXUnionIntFirst,
+                  R"({"i" : "1", "isu":{"variant_i":"42"}})", 1,
+                  GetIntUnionPtr<xisu>(42));
+
+TEST_WIRE_TO_JSON(
+    NullableXUnionIntFirstStruct, NullableXUnionIntFirst,
+    R"({"i": "1", "isu":{"variant_tss":{"value1":"harpo","value2":"chico"}}})",
+    1, GetStructUnionPtr<xisu>("harpo", "chico"));
 
 namespace {
 
-test::fidlcat::examples::u8_u16_union GetUInt8Union(uint8_t i) {
-  test::fidlcat::examples::u8_u16_union u;
+using uuu = test::fidlcat::examples::u8_u16_union;
+using uux = test::fidlcat::examples::u8_u16_xunion;
+
+template <class T>
+T GetUInt8Union(uint8_t i) {
+  T u;
   u.set_variant_u8(i);
   return u;
 }
 
-test::fidlcat::examples::u8_u16_union GetUInt16Union(uint16_t i) {
-  test::fidlcat::examples::u8_u16_union u;
+template <class T>
+T GetUInt16Union(uint16_t i) {
+  T u;
   u.set_variant_u16(i);
   return u;
 }
@@ -533,12 +569,20 @@ test::fidlcat::examples::u8_u16_union GetUInt16Union(uint16_t i) {
 }  // namespace
 
 TEST_WIRE_TO_JSON(ShortUnion8, ShortUnion,
-                  R"({"u":{"variant_u8":"16"}, "i":"1"})", GetUInt8Union(16),
-                  1);
+                  R"({"u":{"variant_u8":"16"}, "i":"1"})",
+                  GetUInt8Union<uuu>(16), 1);
 
 TEST_WIRE_TO_JSON(ShortUnion16, ShortUnion,
                   R"({"u":{"variant_u16":"1024"}, "i":"1"})",
-                  GetUInt16Union(1024), 1);
+                  GetUInt16Union<uuu>(1024), 1);
+
+TEST_WIRE_TO_JSON(ShortXUnion8, ShortXUnion,
+                  R"({"u":{"variant_u8":"16"}, "i":"1"})",
+                  GetUInt8Union<uux>(16), 1);
+
+TEST_WIRE_TO_JSON(ShortXUnion16, ShortXUnion,
+                  R"({"u":{"variant_u16":"1024"}, "i":"1"})",
+                  GetUInt16Union<uux>(1024), 1);
 
 // Enum Tests
 
diff --git a/tools/fidlcat/lib/wire_types.cc b/tools/fidlcat/lib/wire_types.cc
index 948260182cd329fd41ba28e9a688c53009ad8f18..56a45587c9e1bf7d26fd69a041c0e1ab8ccee896 100644
--- a/tools/fidlcat/lib/wire_types.cc
+++ b/tools/fidlcat/lib/wire_types.cc
@@ -278,6 +278,9 @@ class Envelope {
     return oss.str();
   }
 
+  static constexpr size_t INLINE_SIZE =
+      sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t);
+
  private:
   const uint8_t* ptr_;
 };
@@ -326,7 +329,7 @@ class EnvelopeType : public Type {
   }
 
   virtual size_t InlineSize() const override {
-    return sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t);
+    return Envelope::INLINE_SIZE;
   }
 
  private:
@@ -444,6 +447,73 @@ Marker UnionType::GetValueCallback(Marker marker, size_t length,
 
 size_t UnionType::InlineSize() const { return union_.size(); }
 
+XUnionType::XUnionType(const XUnion& uni, bool is_nullable)
+    : xunion_(uni), is_nullable_(is_nullable) {}
+
+Marker XUnionType::GetValueCallback(Marker marker, size_t length,
+                                    ObjectTracker* tracker,
+                                    ValueGeneratingCallback& callback) const {
+  const uint8_t* bytes = marker.byte_pos();
+  // Advance by the size of the ordinal + padding.
+  marker.AdvanceBytesBy(sizeof(uint64_t));
+  if (!marker.is_valid()) {
+    return marker;
+  }
+
+  uint32_t ordinal = internal::MemoryFrom<uint32_t>(bytes);
+  if (ordinal == 0) {
+    if (!is_nullable_) {
+      FXL_LOG(WARNING) << "Encoding error: found null value in non-nullable "
+                          "xunion.  This is likely a bug in the FIDL binding, "
+                          "so contact the FIDL binding authors.";
+    }
+    Envelope envelope(marker.byte_pos());
+    FXL_CHECK(envelope.num_bytes() == 0 && envelope.num_handles() == 0 &&
+              envelope.pointer() == 0)
+        << "Undefined ordinal in xunion without empty envelope.";
+    callback = NullCallback(true);
+    marker.AdvanceBytesBy(Envelope::INLINE_SIZE);
+    return marker;
+  }
+  std::unique_ptr<Type> target_type;
+  std::string member_name;
+  for (auto& member : xunion_.members()) {
+    std::optional<Ordinal> maybe_target_ordinal = member.ordinal();
+    if (!maybe_target_ordinal) {
+      continue;
+    }
+    Ordinal target_ordinal = *maybe_target_ordinal;
+    if (target_ordinal == ordinal) {
+      target_type = member.GetType();
+      member_name = member.name();
+      break;
+    }
+  }
+  if (target_type == nullptr) {
+    // Need to figure out what to do with this...
+    member_name = "unknown$";
+    member_name.append(std::to_string(ordinal));
+  }
+
+  EnvelopeType type(target_type.release());
+  ValueGeneratingCallback value_callback;
+  marker =
+      type.GetValueCallback(marker, type.InlineSize(), tracker, value_callback);
+  callback = [member_name, value_callback = std::move(value_callback)](
+                 ObjectTracker* tracker, Marker& marker,
+                 rapidjson::Value& value,
+                 rapidjson::Document::AllocatorType& allocator) mutable {
+    value.SetObject();
+    tracker->ObjectEnqueue(member_name, std::move(value_callback), value,
+                           allocator);
+    return true;
+  };
+
+  return marker;
+}
+
+size_t XUnionType::InlineSize() const { return xunion_.size(); }
+
 PointerType::PointerType(Type* target_type, bool keep_null)
     : target_type_(target_type), keep_null_(keep_null) {}
 
diff --git a/tools/fidlcat/lib/wire_types.h b/tools/fidlcat/lib/wire_types.h
index 3f235fef98a9f97452518edf4a066bdef6288c32..6c376bcaf085fac2339627c92550919db8c5d3af 100644
--- a/tools/fidlcat/lib/wire_types.h
+++ b/tools/fidlcat/lib/wire_types.h
@@ -112,9 +112,9 @@ class ObjectTracker {
   // constructor) + the given offset.
   bool RunCallbacksFrom(Marker& marker);
 
-  void MessageEnqueue(
-      ValueGeneratingCallback&& callback, rapidjson::Value& target_object,
-      rapidjson::Document::AllocatorType& allocator);
+  void MessageEnqueue(ValueGeneratingCallback&& callback,
+                      rapidjson::Value& target_object,
+                      rapidjson::Document::AllocatorType& allocator);
 
   // Enqueues a callback to be executed when running RunCallbacksFrom.
   // |key| is the JSON key it will construct.
@@ -377,6 +377,21 @@ class UnionType : public Type {
   const Union& union_;
 };
 
+class XUnionType : public Type {
+ public:
+  XUnionType(const XUnion& uni, bool is_nullable);
+
+  virtual Marker GetValueCallback(
+      Marker marker, size_t length, ObjectTracker* tracker,
+      ValueGeneratingCallback& callback) const override;
+
+  virtual size_t InlineSize() const override;
+
+ private:
+  const XUnion& xunion_;
+  const bool is_nullable_;
+};
+
 // A type that can be used to express that this is a pointer to an instance of
 // another type.
 class PointerType : public Type {