diff --git a/bin/input-monitor.c b/bin/input-monitor.c
index 6d5bc0f0136d83efd1a42f935adf3be744c2efae..6234d23a8ed4ca70cbf8f92b6c83926363f42d43 100644
--- a/bin/input-monitor.c
+++ b/bin/input-monitor.c
@@ -30,6 +30,7 @@
 #include "steam-runtime-tools/glib-backports-internal.h"
 #include "steam-runtime-tools/input-device-internal.h"
 #include "steam-runtime-tools/json-glib-backports-internal.h"
+#include "steam-runtime-tools/json-utils-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
 
 #include <stdio.h>
@@ -106,44 +107,6 @@ print_json (JsonBuilder *builder)
     g_warning ("Unable to write final newline: %s", g_strerror (errno));
 }
 
-static void
-add_uevent_member (JsonBuilder *builder,
-                   const char *name,
-                   const char *value)
-{
-  const char *start = value;
-  const char *end;
-
-  /* value is conceptually a single string, but we
-   * present it as an array of lines here, because that's
-   * a lot easier to read! */
-  json_builder_set_member_name (builder, name);
-  json_builder_begin_array (builder);
-
-  while (*start != '\0')
-    {
-      g_autofree gchar *valid = NULL;
-
-      while (*start == '\n')
-        start++;
-
-      if (*start == '\0')
-        break;
-
-      end = strchrnul (start, '\n');
-
-      valid = g_utf8_make_valid (start, end - start);
-      json_builder_add_string_value (builder, valid);
-
-      if (*end == '\0')
-        break;
-
-      start = end + 1;
-    }
-
-  json_builder_end_array (builder);
-}
-
 static void
 append_evdev_hex (GString *buf,
                   const unsigned long *bits,
@@ -576,7 +539,7 @@ added (SrtInputDeviceMonitor *monitor,
               g_autofree gchar *uevent = srt_input_device_dup_uevent (dev);
 
               if (uevent != NULL)
-                add_uevent_member (builder, "uevent", uevent);
+                _srt_json_builder_add_array_of_lines (builder, "uevent", uevent);
             }
 
           sys_path = srt_input_device_get_hid_sys_path (dev);
@@ -628,7 +591,7 @@ added (SrtInputDeviceMonitor *monitor,
                   json_builder_add_string_value (builder, id.phys);
 
                   if (uevent != NULL)
-                    add_uevent_member (builder, "uevent", uevent);
+                    _srt_json_builder_add_array_of_lines (builder, "uevent", uevent);
                 }
 
               json_builder_end_object (builder);
@@ -686,7 +649,7 @@ added (SrtInputDeviceMonitor *monitor,
                   json_builder_add_string_value (builder, id.phys);
 
                   if (uevent != NULL)
-                    add_uevent_member (builder, "uevent", uevent);
+                    _srt_json_builder_add_array_of_lines (builder, "uevent", uevent);
                 }
 
               json_builder_end_object (builder);
@@ -739,7 +702,7 @@ added (SrtInputDeviceMonitor *monitor,
                   g_autofree gchar *uevent = srt_input_device_dup_usb_device_uevent (dev);
 
                   if (uevent != NULL)
-                    add_uevent_member (builder, "uevent", uevent);
+                    _srt_json_builder_add_array_of_lines (builder, "uevent", uevent);
                 }
 
               json_builder_end_object (builder);
diff --git a/bin/system-info.c b/bin/system-info.c
index 346c0fde7cdfd29bc883375bbf6fea48a933c74c..69667a6de7c4869c96ab26503fcac226bb01f2fc 100644
--- a/bin/system-info.c
+++ b/bin/system-info.c
@@ -25,93 +25,7 @@
 
 /*
  * Output basic information about the system on which the tool is run.
- * The output is a JSON object with the following keys:
- *
- * can-write-uinput:
- *   The values are boolean: true if we can write to `/dev/uninput`,
- *   false if we are not able to do it.
- *
- * architectures:
- *   An object. The keys are multiarch tuples like %SRT_ABI_I386,
- *   as used in Debian and the freedesktop.org SDK runtime.
- *   The values are objects with more details of the architecture:
- *
- *   can-run:
- *     The values are boolean: true if we can definitely run
- *     executables of this architecture, or false if we cannot prove
- *     that we can do so.
- *
- *   library-issues-summary:
- *     A string array listing all the libraries problems that has been found
- *     in the running system. Possible values can be: "cannot-load",
- *     "missing-symbols", "misversioned-symbols" and "internal-error".
- *     If "can-run" for this architecture is false we skip the library check
- *     and this "library-issues-summary" will not be printed at all.
- *
- *   library-details:
- *     An object. The keys are library SONAMEs, such as `libjpeg.so.62`.
- *     The values are objects with more details of the library:
- *
- *     path:
- *       The value is a string representing the full path about where the
- *       @library has been found. The value is `null` if the library is
- *       not available in the system.
- *
- *     messages:
- *       If present, the value is a string containing diagnostic messages
- *       that were encountered when attempting to load the library.
- *       This member will only be available if there were some messages.
- *
- *     issues:
- *       A string array listing all the @library problems that has been
- *       found in the running system. For possible values check
- *       `library-issues-summary`. This object will be available only if
- *       there are some issues.
- *
- *     missing-symbols:
- *       A string array listing all the symbols that were expected to be
- *       provided by @library but were not found. This object will be
- *       available only if there are sone missing symbols.
- *
- *     misversioned-symbols:
- *       A string array listing all the symbols that were expected to be
- *       provided by @library but were available with a different version.
- *       This object will be available only if there are some misversioned
- *       symbols.
- *
- * graphics:
- *   An object. The keys are multiarch tuples like %SRT_ABI_I386,
- *   as used in Debian and the freedesktop.org SDK runtime.
- *   The values are objects with more details of the graphics results:
- *
- * locale-issues:
- *  A string array listing locale-related issues.
- *
- * locales:
- *  An object. The keys are either `<default>` (representing passing
- *  the empty string to `setlocale()`), or locale names that can be
- *  requested with `setlocale()`. They will include at least `C`,
- *  `C.UTF-8`, `en_US.UTF-8` and `<default>`, and may include more
- *  in future versions of steam-runtime-tools. The values are objects
- *  containing either:
- *
- *    error:
- *      A string: The error that was encountered when trying to
- *      set this locale
- *    error-domain:
- *      A string: The GError domain
- *    error-code:
- *      An integer: The GError code
- *
- *  or:
- *
- *    resulting-name:
- *      A string: the locale name as returned by setlocale(), if
- *      different
- *    charset:
- *      A string: the character set
- *    is_utf8:
- *      A boolean: whether the character set is UTF-8
+ * See system-info.md for details.
  */
 
 #include <libglnx.h>
@@ -364,10 +278,7 @@ print_libraries_details (JsonBuilder *builder,
           messages = srt_library_get_messages (l->data);
 
           if (messages != NULL)
-            {
-              json_builder_set_member_name (builder, "messages");
-              json_builder_add_string_value (builder, messages);
-            }
+            _srt_json_builder_add_array_of_lines (builder, "messages", messages);
 
           json_builder_set_member_name (builder, "soname");
           json_builder_add_string_value (builder, soname);
@@ -434,10 +345,7 @@ print_graphics_details(JsonBuilder *builder,
       messages = srt_graphics_get_messages (g->data);
 
       if (messages != NULL)
-        {
-          json_builder_set_member_name (builder, "messages");
-          json_builder_add_string_value (builder, messages);
-        }
+        _srt_json_builder_add_array_of_lines (builder, "messages", messages);
 
       json_builder_set_member_name (builder, "renderer");
       json_builder_add_string_value (builder, srt_graphics_get_renderer_string (g->data));
@@ -1398,10 +1306,7 @@ main (int argc,
       json_builder_end_array (builder);
 
       if (xdg_portal_messages != NULL)
-        {
-          json_builder_set_member_name (builder, "messages");
-          json_builder_add_string_value (builder, xdg_portal_messages);
-        }
+        _srt_json_builder_add_array_of_lines (builder, "messages", xdg_portal_messages);
     }
   json_builder_end_object (builder);
 
diff --git a/bin/system-info.md b/bin/system-info.md
index 94e04b7faa8867e3f542e8268bd7a93c60e297f5..88bdeea84726361f4d7fce62bbe3798c3928ab84 100644
--- a/bin/system-info.md
+++ b/bin/system-info.md
@@ -431,6 +431,9 @@ keys:
             symbol exists with an unexpected version or no version, they
             are listed here.
 
+        **messages**
+        :   Human-readable array of diagnostic messages.
+
     **graphics-details**
     :   An object representing graphics stacks. The keys are strings
         such as **glx/gl**, **egl_x11/glesv2** or **x11/vulkan**
@@ -498,6 +501,9 @@ keys:
             :   The test to see if drawing works for the given graphics mode
                 had an error and was unable to draw.
 
+        **messages**
+        :   Human-readable array of diagnostic messages.
+
     **dri_drivers**
     :   An array of objects describing the Mesa DRI driver modules that have been
         found. These drivers are used by Mesa and by older versions of Xorg.
@@ -777,7 +783,7 @@ keys:
         :   There isn't a working XDG portal implementation.
 
     **messages**
-    :   Human-readable string with error messages about the XDG portal
+    :   Human-readable array of lines with error messages about the XDG portal
         support. This element will not be printed if there aren't any error
         messages.
 
diff --git a/steam-runtime-tools/graphics.c b/steam-runtime-tools/graphics.c
index dbff33ee6f7bd2f3bf90b7c4465213553f154c75..20d1a99937dbfcdfbbcf4efa700d9e94609609e3 100644
--- a/steam-runtime-tools/graphics.c
+++ b/steam-runtime-tools/graphics.c
@@ -1498,7 +1498,7 @@ _srt_graphics_get_from_report (JsonObject *json_obj,
       graphics_members = json_object_get_members (json_graphics_obj);
       for (GList *l = graphics_members; l != NULL; l = l->next)
         {
-          const gchar *messages = NULL;
+          g_autofree gchar *messages = NULL;
           const gchar *renderer = NULL;
           const gchar *version = NULL;
           int exit_status;
@@ -1545,8 +1545,7 @@ _srt_graphics_get_from_report (JsonObject *json_obj,
               continue;
             }
 
-          messages = json_object_get_string_member_with_default (json_stack_obj, "messages",
-                                                                 NULL);
+          messages = _srt_json_object_dup_array_of_lines_member (json_stack_obj, "messages");
           renderer = json_object_get_string_member_with_default (json_stack_obj, "renderer",
                                                                  NULL);
           version = json_object_get_string_member_with_default (json_stack_obj, "version",
diff --git a/steam-runtime-tools/json-utils-internal.h b/steam-runtime-tools/json-utils-internal.h
index f01ddf524ac740d586b0ec347632a5f91cbedd66..32dced70604470d2f6b38e19f0a40c3dee5e9eb0 100644
--- a/steam-runtime-tools/json-utils-internal.h
+++ b/steam-runtime-tools/json-utils-internal.h
@@ -46,6 +46,9 @@ gchar ** _srt_json_object_dup_strv_member (JsonObject *json_obj,
 
 gchar *_srt_json_object_dup_array_of_lines_member (JsonObject *json_obj,
                                                    const gchar *array_member);
+void   _srt_json_builder_add_array_of_lines       (JsonBuilder *builder,
+                                                   const char *name,
+                                                   const char *value);
 
 void _srt_json_builder_add_strv_value (JsonBuilder *builder,
                                        const gchar *array_name,
diff --git a/steam-runtime-tools/json-utils.c b/steam-runtime-tools/json-utils.c
index e4f42687d2b3794e627378ae2993e77039e53118..09d7d67276a8214bab92361a81b7e7a0cf44c51e 100644
--- a/steam-runtime-tools/json-utils.c
+++ b/steam-runtime-tools/json-utils.c
@@ -148,8 +148,12 @@ _srt_json_object_dup_strv_member (JsonObject *json_obj,
  *
  * If @json_obj has a member named @array_member and it is an array
  * of strings, concatenate the strings (adding a trailing newline to
- * each one if not already present) and return them. Otherwise,
- * return %NULL.
+ * each one if not already present) and return them.
+ *
+ * For compatibility with the old representation of diagnostic messages,
+ * if @array_member exists but is a single string, return it.
+ *
+ * Otherwise, return %NULL.
  *
  * Returns: (transfer full) (element-type utf8) (nullable):
  *  A string, or %NULL if not found. Free with g_free().
@@ -168,7 +172,13 @@ _srt_json_object_dup_array_of_lines_member (JsonObject *json_obj,
 
   arr_node = json_object_get_member (json_obj, array_member);
 
-  if (arr_node == NULL || !JSON_NODE_HOLDS_ARRAY (arr_node))
+  if (arr_node == NULL || JSON_NODE_HOLDS_NULL (arr_node))
+    return NULL;
+
+  if (JSON_NODE_HOLDS_VALUE (arr_node))
+    return json_node_dup_string (arr_node);
+
+  if (!JSON_NODE_HOLDS_ARRAY (arr_node))
     return NULL;
 
   array = json_node_get_array (arr_node);
@@ -193,6 +203,77 @@ _srt_json_object_dup_array_of_lines_member (JsonObject *json_obj,
   return g_string_free (g_steal_pointer (&ret), FALSE);
 }
 
+/**
+ * _srt_json_builder_add_array_of_lines:
+ * @builder: (not nullable): A JSON builder to which the provided @value
+ *  will be added
+ * @name: (not nullable): The array member name to use
+ * @value: (nullable): String to be split into an array of lines, or %NULL
+ *
+ * Write an array of lines into a JSON object.
+ *
+ * A non-%NULL @value will be split into lines, removing any trailing
+ * newlines, so
+ *
+ * |[
+ * if (g_file_get_contents ("/etc/passwd", &contents, ...))
+ *   _srt_json_builder_add_array_of_lines (builder, "/etc/passwd", contents);
+ * ]|
+ *
+ * might produce
+ *
+ * |[
+ * "/etc/passwd" : [
+ *   "root:x:0:0:root:/root:/bin/bash",
+ *   ...,
+ *   "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
+ * ],
+ * ]|
+ *
+ * A %NULL @value will be emitted as the special JSON constant `null`.
+ */
+void
+_srt_json_builder_add_array_of_lines (JsonBuilder *builder,
+                                      const char *name,
+                                      const char *value)
+{
+  const char *start = value;
+  const char *end;
+
+  json_builder_set_member_name (builder, name);
+
+  if (value == NULL)
+    {
+      json_builder_add_string_value (builder, NULL);
+      return;
+    }
+
+  json_builder_begin_array (builder);
+
+  while (*start != '\0')
+    {
+      g_autofree gchar *valid = NULL;
+
+      while (*start == '\n')
+        start++;
+
+      if (*start == '\0')
+        break;
+
+      end = strchrnul (start, '\n');
+
+      valid = g_utf8_make_valid (start, end - start);
+      json_builder_add_string_value (builder, valid);
+
+      if (*end == '\0')
+        break;
+
+      start = end + 1;
+    }
+
+  json_builder_end_array (builder);
+}
+
 /**
  * _srt_json_builder_add_strv_value:
  * @builder: (not nullable): A JSON Builder where the provided @values
diff --git a/steam-runtime-tools/xdg-portal.c b/steam-runtime-tools/xdg-portal.c
index 53e196dcda37ed5a3c75a80cd3705ce1bc8e499d..8cb5aba1ceb2eeca9a0cd9d034445ff7affd2651 100644
--- a/steam-runtime-tools/xdg-portal.c
+++ b/steam-runtime-tools/xdg-portal.c
@@ -710,7 +710,7 @@ _srt_xdg_portal_get_info_from_report (JsonObject *json_obj)
   g_autoptr(GPtrArray) backends = g_ptr_array_new_full (2, g_object_unref);
   g_autoptr(GPtrArray) interfaces = g_ptr_array_new_full (2, g_object_unref);
   SrtXdgPortalIssues issues = SRT_XDG_PORTAL_ISSUES_NONE;
-  const gchar *messages = NULL;
+  g_autofree gchar *messages = NULL;
 
   g_return_val_if_fail (json_obj != NULL, NULL);
 
@@ -727,7 +727,7 @@ _srt_xdg_portal_get_info_from_report (JsonObject *json_obj)
                                           "issues",
                                           SRT_XDG_PORTAL_ISSUES_UNKNOWN);
 
-  messages = json_object_get_string_member_with_default (json_portals_obj, "messages", NULL);
+  messages = _srt_json_object_dup_array_of_lines_member (json_portals_obj, "messages");
 
   if (!json_object_has_member (json_portals_obj, "details"))
     return _srt_xdg_portal_new (messages, issues, NULL, NULL);
diff --git a/tests/json-report/full-good-report.json b/tests/json-report/full-good-report.json
index d926f4b07f7010e7a60a1a95699645f2d3745360..05b01afda1950c36f802cd68bc62394135c8e538 100644
--- a/tests/json-report/full-good-report.json
+++ b/tests/json-report/full-good-report.json
@@ -61,7 +61,10 @@
       },
       "graphics-details" : {
         "x11/vulkan" : {
-          "messages" : "ERROR: [Loader Message] Code 0 : /usr/lib/amdvlk64.so: wrong ELF class: ELFCLASS64\nCannot create Vulkan instance.\n",
+          "messages" : [
+            "ERROR: [Loader Message] Code 0 : /usr/lib/amdvlk64.so: wrong ELF class: ELFCLASS64",
+            "Cannot create Vulkan instance."
+          ],
           "renderer" : null,
           "version" : null,
           "issues" : [
@@ -146,7 +149,11 @@
           "version" : null
         },
         "glx/gl" : {
-          "messages" : "libGL: Can't open configuration file /etc/drirc: No such file or directory.\n/usr/share/libdrm/amdgpu.ids version: 1.0.0\nlibGL: Using DRI3 for screen 0\n",
+          "messages" : [
+            "libGL: Can't open configuration file /etc/drirc: No such file or directory.",
+            "/usr/share/libdrm/amdgpu.ids version: 1.0.0",
+            "libGL: Using DRI3 for screen 0"
+          ],
           "renderer" : "AMD Radeon RX 5700 XT (NAVI10, DRM 3.36.0, 5.6.7-arch1-1, LLVM 10.0.0)",
           "version" : "4.6 (Compatibility Profile) Mesa 20.0.5",
           "library-vendor" : "glvnd",