diff --git a/CMakeLists.txt b/CMakeLists.txt
index db4e8c246ea3bbaadd4265bdb9164407120688aa..6367ddf4bcef906299c21eec02d56168e036cf51 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -101,6 +101,8 @@ option(LWS_WITHOUT_DAEMONIZE "Don't build the daemonization api" ON)
 option(LWS_SSL_SERVER_WITH_ECDH_CERT "Include SSL server use ECDH certificate" OFF)
 option(LWS_WITH_LEJP "With the Lightweight JSON Parser" ON)
 option(LWS_WITH_SQLITE3 "Require SQLITE3 support" OFF)
+option(LWS_WITH_STRUCT_JSON "Generic struct serialization to and from JSON" ON)
+option(LWS_WITH_STRUCT_SQLITE3 "Generic struct serialization to and from SQLITE3" OFF)
 option(LWS_WITH_SMTP "Provide SMTP support" OFF)
 if (WIN32 OR LWS_WITH_ESP32)
 option(LWS_WITH_DIR "Directory scanning api support" OFF)
@@ -189,6 +191,10 @@ if (NOT LWS_WITH_NETWORK)
 	set(LWS_WITH_POLL 0)
 endif()
 
+if (LWS_WITH_STRUCT_SQLITE3)
+	set(LWS_WITH_SQLITE3 1)
+endif()
+
 # do you care about this?  Then send me a patch where it disables it on travis
 # but allows it on APPLE
 if (APPLE)
@@ -898,7 +904,10 @@ set(HDR_PUBLIC
 
 set(SOURCES
 	lib/core/alloc.c
+	lib/core/buflist.c
 	lib/core/context.c
+	lib/core/lws_dll.c
+	lib/core/lws_dll2.c
 	lib/core/libwebsockets.c
 	lib/core/logs.c
 	lib/misc/base64-decode.c
@@ -1020,6 +1029,16 @@ if (LWS_WITH_DISKCACHE)
 		lib/misc/diskcache.c)
 endif()
 
+if (LWS_WITH_STRUCT_JSON)
+	list(APPEND SOURCES
+		lib/misc/lws-struct-lejp.c)
+endif()
+
+if (LWS_WITH_STRUCT_SQLITE3)
+	list(APPEND SOURCES
+		lib/misc/lws-struct-sqlite.c)
+endif()
+
 if (NOT LWS_WITHOUT_CLIENT)
 	list(APPEND SOURCES
 		lib/core-net/connect.c
diff --git a/READMEs/README.lws_struct.md b/READMEs/README.lws_struct.md
new file mode 100644
index 0000000000000000000000000000000000000000..00ca08abd2d7faf913ba728ce12c8040b61aa6f4
--- /dev/null
+++ b/READMEs/README.lws_struct.md
@@ -0,0 +1,38 @@
+# lws_struct
+
+## Overview
+
+lws_struct provides a lightweight method for serializing and deserializing C
+structs to and from JSON, and to and from sqlite3.
+
+![lws_struct overview](../doc-assets/lws_struct-overview.svg)
+
+ - you provide a metadata array describing struct members one-time, then call
+   generic apis to serialize and deserialize 
+
+ - supports flat structs, single child struct pointers, and unbounded arrays /
+   linked-lists of child objects automatically using [lws_dll2 linked-lists](./README.lws_dll.md)
+ 
+ - supports boolean and C types char, int, long, long long in explicitly signed
+   and unsigned forms
+ 
+ - supports both char * type string members where the unbounded content is
+   separate and pointed to, and fixed length char array[] type members where
+   the content is part of the struct
+
+ - huge linear strings are supported by storing to a temp lwsac of chained chunks,
+   which is written into a single linear chunk in the main lwsac once the
+   total string length is known
+   
+ - deserialization allocates into an [lwsac](../lib/misc/lwsac/README.md), so everything is inside as few
+   heap allocations as possible while still able to expand to handle arbitrary
+   array or strins sizes
+   
+ - when deserialized structs are finished with, a single call to free the
+   lwsac frees the whole thing without having to walk it
+   
+ - stateful serializaton and deserialization allows as-you-get packets incremental
+   parsing and production of chunks of as-you-can-send incremental serialization
+   output cleanly
+
+## Examples
diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in
index 70cfa59bc16a5af236590f5a442718d653c71477..5b1be9b61653b7b41c3c53363eb0a201bbfaa698 100644
--- a/cmake/lws_config.h.in
+++ b/cmake/lws_config.h.in
@@ -116,6 +116,8 @@
 #cmakedefine LWS_WITH_SOCKS5
 #cmakedefine LWS_WITH_STATEFUL_URLDECODE
 #cmakedefine LWS_WITH_STATS
+#cmakedefine LWS_WITH_STRUCT_SQLITE3
+#cmakedefine LWS_WITH_SQLITE3
 #cmakedefine LWS_WITH_THREADPOOL
 #cmakedefine LWS_WITH_TLS
 #cmakedefine LWS_WITH_UNIX_SOCK
diff --git a/doc-assets/lws_struct-overview.svg b/doc-assets/lws_struct-overview.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ba618c9a03e15be7f67f6a1d2ac713d22a010a10
--- /dev/null
+++ b/doc-assets/lws_struct-overview.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="258.28mm" height="107.81mm" version="1.1" viewBox="0 0 258.28 107.81" xmlns="http://www.w3.org/2000/svg">
+	<defs>
+		<marker id="a" overflow="visible" orient="auto">
+			<path transform="matrix(.2 0 0 .2 1.2 0)" d="m0 0 5-5-17.5 5 17.5 5z" fill-rule="evenodd" stroke="#000" stroke-width="1pt"/>
+		</marker>
+		<marker id="e" overflow="visible" orient="auto">
+			<path transform="matrix(-.3 0 0 -.3 .69 0)" d="m8.7186 4.0337-10.926-4.0177 10.926-4.0177c-1.7455 2.3721-1.7354 5.6175-6e-7 8.0354z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".625"/>
+		</marker>
+		<marker id="b" overflow="visible" orient="auto">
+			<path transform="matrix(.2 0 0 .2 1.2 0)" d="m0 0 5-5-17.5 5 17.5 5z" fill-rule="evenodd" stroke="#000" stroke-width="1pt"/>
+		</marker>
+		<marker id="f" overflow="visible" orient="auto">
+			<path transform="matrix(-.3 0 0 -.3 .69 0)" d="m8.7186 4.0337-10.926-4.0177 10.926-4.0177c-1.7455 2.3721-1.7354 5.6175-6e-7 8.0354z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".625"/>
+		</marker>
+		<marker id="c" overflow="visible" orient="auto">
+			<path transform="matrix(.2 0 0 .2 1.2 0)" d="m0 0 5-5-17.5 5 17.5 5z" fill-rule="evenodd" stroke="#000" stroke-width="1pt"/>
+		</marker>
+		<marker id="g" overflow="visible" orient="auto">
+			<path transform="matrix(-.3 0 0 -.3 .69 0)" d="m8.7186 4.0337-10.926-4.0177 10.926-4.0177c-1.7455 2.3721-1.7354 5.6175-6e-7 8.0354z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".625"/>
+		</marker>
+		<filter id="d" x="-.03148" y="-.082687" width="1.063" height="1.1654" color-interpolation-filters="sRGB">
+			<feGaussianBlur stdDeviation="1.2468962"/>
+		</filter>
+	</defs>
+	<g transform="translate(751.29 58.975)">
+		<g>
+			<rect transform="matrix(2.5561 0 0 2.5561 1169.1 91.771)" x="-748.3" y="-55.983" width="95.061" height="36.191" filter="url(#d)"/>
+			<rect x="-744.61" y="-52.171" width="242.99" height="92.508" fill="#fff"/>
+			<circle cx="-715.52" cy="-2.6399" r="13.375" fill="#d4aa00"/>
+			<text x="-715.49457" y="-0.013140791" dominant-baseline="auto" fill="#000000" font-family="'Open Sans Condensed'" font-size="9.0174px" letter-spacing="0px" stroke-width=".6763" text-align="center" text-anchor="middle" word-spacing="0px" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-715.49457" y="-0.013140791" stroke-width=".6763">sqlite</tspan></text>
+			<circle cx="-671.6" cy="-2.9116" r="13.375" fill="#b3b3b3"/>
+			<text x="-671.56494" y="-0.28489438" dominant-baseline="auto" fill="#000000" font-family="'Open Sans Condensed'" font-size="9.0174px" letter-spacing="0px" stroke-width=".6763" text-align="center" text-anchor="middle" word-spacing="0px" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-671.56494" y="-0.28489438" stroke-width=".6763">C structs</tspan></text>
+			<circle cx="-627.82" cy="-2.67" r="13.375" fill="#aad400"/>
+		</g>
+		<g fill="#000000" font-family="'Open Sans Condensed'" font-size="9.0174px" letter-spacing="0px" stroke-width=".6763" text-anchor="middle" word-spacing="0px">
+			<text x="-627.78656" y="-0.043329135" dominant-baseline="auto" text-align="center" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-627.78656" y="-0.043329135" stroke-width=".6763">JSON</tspan></text>
+			<text x="-627.64801" y="21.166273" dominant-baseline="auto" text-align="center" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-627.64801" y="21.166273" stroke-width=".6763">transit</tspan></text>
+			<text x="-716.26086" y="21.211004" dominant-baseline="auto" text-align="center" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-716.26086" y="21.211004" stroke-width=".6763">storage</tspan></text>
+			<text x="-672.80682" y="20.817616" dominant-baseline="auto" text-align="center" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-672.80682" y="20.817616" stroke-width=".6763">processing</tspan></text>
+		</g>
+		<g fill="none" stroke="#000" stroke-width="1.7147">
+			<path d="m-699.88-2.5175 13.015-0.1388" marker-end="url(#g)" marker-start="url(#c)" stroke-dasharray="3.42934834, 1.71467418"/>
+			<path d="m-655.78-2.6715 13.015-0.1388" marker-end="url(#f)" marker-start="url(#b)"/>
+			<path d="m-611.7-3.5168 13.015-0.1388" marker-end="url(#e)" marker-start="url(#a)"/>
+		</g>
+		<g transform="matrix(.98084 0 0 .98084 -621.3 237.27)" fill="#000080" opacity=".244">
+			<path d="m41.881-252.66c-0.74224 2.5432-0.89347 6.2107 2.3684 9.2701"/>
+			<path d="m53.529-227.18c6.6092-4.0094 3.4326-11.99-0.99884-13.439-1.8556-0.60663-5.8343 0.0294-7.9178 3.1773-0.58269 0.88034-2.1307 3.9439-0.48031 5.5996 2.9809 2.6693 5.6161 0.56537 5.3608-1.4101-0.21066-1.6304-2.5195-2.2246-3.5161-0.16091-0.25975-2.8413 3.6465-3.1988 4.5934-2.4826 3.041 2.3001 1.5823 6.4615 1.0276 7.1414-0.73173 0.89688-3.6971 3.6214-8.2393 0.56437-6.8298-5.655-0.13193-11.313 3.866-12.476 6.1028-2.0341 9.0102 1.5223 10.671 3.6144 3.6868 4.6452 1.9809 8.9627 5.3505 13.525 1.9595 3.128 7.9821 3.7261 11.158 0.60173 5.5288-6.1445 3.3332-13.229-1.2516-13.479-0.22801-0.0125-2.9101-0.63086-4.4241 2.3627-1.0649 2.794 4e-3 3.8072 0.81726 4.1078 2.9088 0.79976 4.5016-5.174 0.82228-3.1128 4.8236-4.2183 5.2182 0.96693 5.0018 2.2308-0.80165 5.564-9.3442 7.4818-10.819 2.8668-0.35205-0.62503-1.4338-5.0762 3.9067-8.1954 5.5765-3.6326 13.211 5.2134 17.982 5.8918 10.106 1.6835 14.307-6.6479 10.856-12.582-1.2554-1.7669-2.8684-3.5441-5.7068-3.7758-6.1109-0.17391-9.2141 3.4739-7.9593 8.2436 1.3959 2.9346 4.6564 2.9335 5.6551 2.6689 10.271-4.9733-4.2693-8.9086-0.87901-1.2245-4.8802-3.8972 0.63964-7.5993 4.4048-6.1991 0.96063 0.35724 5.1803 5.0648-0.05897 9.3556-1.1663 0.95517-4.5269 2.2051-8.0004 0.28109-3.3694-1.8664-3.5338-5.8701-2.477-9.261 1.0569-3.3909 5.3014-5.0232 8.3812-4.6644 6.7277 0.78392 8.7315 5.091 9.5955 7.5454 2.4366 6.9213-4.6408 13.804-8.902 13.4-4.2109 0.0282-8.4677 0.21333-12.52-6.2854 2.1202 3.9189 2.0498 12.14-3.7631 14.191-4.5313 1.8257-11.466 2.5989-16.992-3.7837-8.6613 4.8198-20.705 1.566-24.156-7.0006-0.92495-2.578-0.47339-6.8624 0.24474-9.4071-7.6693 1.5897-10.613-4.3062-9.6483-8.4011 0.91606-4.9175 6.1951-6.9841 9.8568-7.0428 1.5425-0.0247 4.6646-0.23374 6.5237 1.4445 0.91307 0.82426 0.56096-3.1566 4.8371-7.1831 1.0571-1.2067 4.9098-4.4309 6.8716-4.0876 2.9444 0.51523 6.0699-5.719 15.148-4.942 3.6186 0.30968 11.982 4.7173 15.123 5.2824 6.4081 1.1531 11.088-2.2 14.812 1.3207 2.1216 2.0055 4.2287 8.5906 1.9089 12.056-3.3431 5.2297 0.78824 7.9425 0.71094 11.137-0.41729 1.0853-2.8169-2.0166-3.8145-6.0592-0.12602-0.51067-1.1007-2.262-2.1876-2.0061 5.211-1.0111 1.7392-4.8923 3.8526-5.3799-1.3772 2.9802-4.6943 6.3834-11.832 6.001 2.3202-2.0058 7.0672-2.1803 9.3859-7.409 1.4893-3.3583-1.1162-6.6918-1.6409-7.4946-1.7761-2.7172-9.3712-0.53945-10.376 0.18939-1.5682 1.1375-2.5616 5.9494-0.43858 6.8512 0.54995 0.23361 1.794 0.56034 3.5295 0.0399 0.50862-0.15251 2.2807-1.4805 1.5209-3.1415-0.75989-1.661-4.1817-1.9196-3.2935 2.0787-2.4306-2.4398 0.42917-5.7946 3.2902-5.3916 0.79279 0.11167 2.0901 0.64339 2.7036 1.9892 0.7832 1.7182 0.04247 9.0743-6.3208 9.7095-2.5704-0.23817-4.933-1.0036-6.0081-4.2173-1.0894-3.2565-0.34649-6.6133 3.6075-8.8417-10.088-5.6172-13.564-5.3423-14.931-5.3234-3.5985 0.0499-11.415 2.2097-12.233 6.9656-2.0539 9.4145 6.0196 15.718 11.313 13.869 2.0435-0.71404 5.3178-3.0132 6.2069-5.7695 1.1253-2.7161 0.52067-9.2257-3.7502-9.8187-7.5674-1.0507-7.4178 5.1207-7.3467 5.7392 0.43169 3.7568 3.3857 3.0344 3.6417 2.0547 0.95883-3.6692-2.0573-0.28653-1.6915-1.0436 0.46602-0.96437 3.7458-4.2325 5.2541-0.60127 1.7856 4.2987-2.9558 7.0032-5.6537 6.3922-4.4526-1.0083-5.5626-5.3976-4.766-8.0601 2.1592-7.2172 11.062-5.6806 12.605-5.1024 5.4016 2.0239 6.2208 7.6173 5.5996 10.861-0.49155 2.5665-2.0196 9.0474-10.012 9.7885-4.1054 0.38065-9.5771-2.4666-11.216-5.3882-2.0253-3.6104-3.5546-6.4056-1.5183-12.344 0.49778-1.4517-0.80522-2.6114-2.1376-2.7358-1.8908-0.17651-7.8322 5.0017-8.3951 12.347-0.0049 0.0687 0.16434 3.8731 1.508 5.7369 1.0084 1.3987 1.5571 1.0966 3.0688 0.64569-0.50956 1.6452-2.9656 3.1932-5.2687 0.96907-1.1202-1.0819-1.4076-4.8291-2.1762-7.3776-0.52918-1.7545-1.3818-2.0825-1.8102-2.3238-10.206-1.1153-13.561 6.2269-9.7615 11.16 1.0884 1.2644 4.3886 2.667 7.0879-0.23624 0.9211-1.4945 1.569-5.634-2.188-6.2928-2.8126-0.35877-4.3231 2.5961-2.3307 4.1754 0.37764 0.29935 2.4183 0.24252 2.3959-1.3755 2.2889 2.8495-1.3845 4.3098-3.6108 2.018-1.7839-2.9588 1.0791-5.2149 3.1674-5.1311-0.18791-0.022 5.895-0.84785 4.9726 6.1419-4.9602 11.939 4.7226 21.559 15.224 15.13z"/>
+			<path d="m49.624-251.08c4.0745-1.5884-4.5287-6.9179-2.0933 1.9722 3.3605 7.6759 16.386 13.929 25.972 10.856 6.2802-2.3279 6.422-8.3769 11.798-9.3185-1.4058 0.498-3.0762-0.99605-8.0373 3.492 2.7673-2.8522 0.46911-8.4801 0.5153-10.38 0.81305 6.522-3.3474 14.573-10.648 15.099-9.1475-0.20557-17.567-6.4219-18.317-10.157-1.3969-3.3934 1.9255-4.3416 0.80968-1.5631z"/>
+			<path d="m65.991-266.93c-3.2975 0.0765-7.0461 1.7511-9.1722 4.3069-1.4452 1.8513-1.5312 4.3552-1.5178 6.6306 0.40978 2.9471 0.52755-2.4046 1.0549-3.2704 0.79541-3.0328-0.05729 1.3697 0.19279 2.5077-3e-3 1.4284 0.62634 4.1837 0.98556 4.6393-0.36808-2.9118-0.83127-5.9611-0.0089-8.831 1.1023-1.717-1.0043 5.3378-0.05502 2.4936 0.4507-2.5768 3.4798-4.9071 5.958-5.5887 1.9113-0.44792 3.8363 0.0423 5.7521 0.13584-1.5893-1.1774-4.5231-0.98014-5.3358-0.94935 1.9261-0.94969 4.1318-0.0705 6.1133 0.25724 0.92665 0.28009 6.1313 2.5542 3.7244 1.4918-2.7444-2.1779-4.7658-2.532-8.0506-2.962 2.403-0.17544 4.792 0.0365 7.8314 1.5548 1.0265 0.36413 3.0918 2.8212 3.2862 2.578-1.2943-1.7886-1.656-2.829-5.0375-4.2074-2.3484-0.68646-3.2712-0.65201-5.7213-0.78692z"/>
+			<path d="m58.364-238.03c0.82342 0.96459 1.6468 1.9292 2.4703 2.8938 0.7972 4.0979 0.99109 8.6917 3.935 11.943 1.529 1.3148 3.8631 1.8335 5.5898 0.94221-1.6708 0.0763-3.301-0.33307-4.7173-1.19-1.0351-0.45373-2.6418-3.7051-0.93718-1.5735 1.7152 1.2122 3.8589 2.519 6.0318 1.776 1.6617-0.48301 3.0361-1.5936 3.9635-3.0436 0.658-0.86497-1.5494 1.0574-2.0538 1.3326-1.8192 1.5865-4.345 0.99565-6.3502 0.16388-1.5572-1.0506-3.0636-2.5037-3.4345-4.4401-0.53767-2.1605 0.98445-4.2317 2.4081-5.6982 1.6672-1.9048-2.3171 0.34388-2.1141 1.6664-1.0943 2.7953-0.80067-1.9094 0.29333-2.6286 1.0637-1.4519-1.4052-0.44253-1.242 0.76276-0.18207 0.78531-0.7779 2.569-0.75391 0.63336 0.26016-2.0878-1.6642-2.6196-3.0889-3.5399z"/>
+			<path d="m28.156-247.06c0.92848-3.4966 5.0287-4.4196 8.1746-4.5432 2.7482-0.098 3.9609 2.5301 3.7359 4.9102 1.2078 1.8932 1.8939 5.2494 4.7221 5.2123 1.2565-0.038 4.8122-0.79982 1.7535 0.0858-1.4333 0.12222-3.256 0.27432-3.2963 0.71318-2.8124-0.78131-1.9305 3.4262-3.2886 4.988-0.84011 2.5666 0.06825 5.4727 1.5198 7.6219-2.1035-1.442-2.5373-4.3132-2.6564-6.6511-1.0429 0.61507 0.02588 3.9157-0.70499 1.3105-0.96281-3.0077 0.12061-6.1571 1.2057-8.9638-0.28831 1.356-1.0953 5.2215-0.54995 4.9051 0.17316-2.8227 2.8539-6.6846 0.33375-8.8214-0.47624-1.622-3.5311-2.5422-3.7271-2.9056 1.0824-0.19259 4.4689 1.1444 2.3851-0.89256-1.7961-1.5634-4.5102-0.71819-6.2136 0.55886-0.92245 1.0856-0.90091 1.8597-1.0907 0.0721-0.8937 0.11462-1.6812 1.692-2.3029 2.3998z"/>
+			<path d="m45.236-235.2c1.4188-1.2781 3.3971-2.7545 5.3421-1.602 2.2217 0.66519 3.4478 3.1534 2.884 5.3536-0.35111 2.2009-1.6599 4.5037-3.921 5.1553 2.5987-0.48934 4.4214-2.8736 4.7078-5.4242 0.1482-0.77718 0.32924-3.0423 0.40115-1.0554 0.12287 1.4282 0.10405 3.0167-0.82412 4.1993 1.4662-1.0739 2.0707-2.9731 1.5762-4.6908-0.45478-0.93908-0.35002-2.0493 0.21589-0.60058 0.29431 0.77353 0.85771 2.5136 0.68046 0.59142 0.10183-2.1526-1.1589-3.992-2.5783-5.468-1.6545-1.5072-4.1397-1.0272-6.0679-0.40689-0.86995 0.3702-2.6266 1.5015-2.405 2.1681 1.5976-1.3604 3.7768-2.0397 5.8513-2.0223 1.1343 0.0526 1.9047 0.93003 0.30213 0.39937-1.4706-0.39468-4.1193 0.64258-4.5509 1.1895 1.2514-0.73692 2.9324-1.0773 4.2523-0.52456-2.0155 0.32148-4.9769-2e-3 -5.7173 2.462z"/>
+			<path d="m86.737-261.88c2.7563-0.85852 6.3293-0.86997 7.8693 2.0772 2.368 3.7758-0.10823 8.4824-3.8369 10.086-1.0758 0.54064-4.9583 2.5338-2.4181 1.0435 1.7273-0.88976 5.7959-3.6731 5.9537-6.6596-1.5209 2.8566-5.3829 6.2444-5.4944 5.5818 2.2515-1.9622 5.7254-6.7355 4.4017-8.798-0.18086 2.3574-0.74912 3.2298-1.9174 4.6555 1.3091-2.3149 1.4959-5.674-1.6939-7.182-1.3411-0.35708-2.2776-0.17587-3.6745-0.07 3.191-1.0558 7.3728 0.49056 7.9763 1.8864-0.45062-2.8257-4.689-2.7438-7.1658-2.621z"/>
+			<path d="m75.47-237.09c2.803 1.1663 5.3876 2.75 7.7894 4.5827 1.9392 1.5512 4.4316 2.1984 6.8793 1.8021 2.3819-0.15037 4.6177-1.2797 6.0384-3.2205 1.9103-1.8401 1.691-4.7936 1.1326-7.1714-0.72217-1.5912-1.5238-3.4197-3.1792-4.1842-2.4435-1.655-5.9834-0.92278-8.0631 1.0117-1.5923 0.92202-2.2122 4.5642-1.4908 5.0768-0.0885-1.1957 0.49061-4.0393 1.2515-3.838-0.37329 1.783-0.92585 4.5926 0.50945 5.5734-0.8281-2.4749-0.42119-5.6188 1.6709-7.3533 1.916-0.69165 1.3727 0.0971 0.05383 0.82066-1.0229 0.84309-0.97413 1.8974 0.2291 0.57914 1.6494-1.1529 5.0885-1.3778 6.0631 0.19933-0.90007-0.28761-2.911-1.116-1.022-0.0561 1.9922 0.71869 2.4199 2.8623 2.6766 4.5463 1.1651-3.0198-1.5306-6.5592-4.7208-6.464 2.0653-0.70343 4.5676 1.3093 5.1924 3.4456 0.99083 2.0816 0.44235 4.4699-0.78661 6.3162-0.28779 1.164-3.2359 3.5234-0.83706 1.7868 1.0288-0.62561 1.9327-3.8066 2.276-3.4672-0.62049 2.3505-2.1836 4.9718-4.8418 5.2318-1.685-1.3884-3.9146 1.1436-5.6327 0.0106 1.0791 1.0606 3.711-0.21206 4.0259 0.49175-2.1714 0.97677-4.6633 0.0758-6.5161-1.1913-2.1407-0.61396-3.3669-2.5051-3.7891-4.5991-0.80372-1.169 0.05018-4.2871 0.01116-4.1904-1.3934 1.8704-0.77804 4.5464 0.15394 6.3709-1.4526-1.2408-3.2863-1.6076-5.0744-2.1103z"/>
+			<path d="m50.816-260.85c-4.7428 2.3195-7.1531 8.0201-6.1682 13.101 0.39021 3.199 4.0638 3.4365 5.7719 5.5891 1.5389 0.65182 2.9375 0.63553 0.71682-0.17495-1.5715-1.7161-4.12-3.0931-5.145-4.8013 1.783-0.2772 4.1569 3.8232 4.4273 3.0339-1.8386-2.4567-5.1454-4.5752-4.3129-8.1415-0.08179-1.1106 1.0497 5.0564 0.75955 1.2857-1.2383-2.9507 0.76703-6.6915 3.6364-3.2944 2.2856 2.3184-1.7431 5.7349 1.4032 7.6768 2.9709 2.6687 6.3811 5.045 10.377 5.8104-2.8244-1.2674-8.8144-4.4477-8.8341-5.6956 3.7202 2.8569 8.0143 5.1295 12.555 6.2946 1.903 0.41675 5.1739-0.28816 5.8167-1.6201-1.0633 0.93888-7.2765 1.5085-3.2054 0.5499 2.3524 0.0776 6.2515-3.239 5.786-3.926-3.7911 3.7293-9.9662 4.6807-14.649 2.0879-2.5619-1.4337-5.342-2.8518-7.136-5.2375-1.3113-2.369 3.214 2.47 0.93224-0.61762-1.1318-2.9407-2.6594-5.4934-4.4296-8.0667 0.12899-2.4985 1.7215-1.6882 1.3476 0.44799 1.3267 0.61265 1.9866 5.1062 2.2629 4.4024 3e-3 -2.2907-2.4535-5.7724-1.0966-7.2729 0.70538 5.5047 2.2634-4.336-0.81591-1.4309z"/>
+			<path d="m64.434-258.85c0.50641-2.0361 2.6105-3.0205 4.9975-2.1045 2.2245 1.1612 3.1451 4.1428 2.485 6.4831-0.36288 1.6369-0.91876 2.8077-1.8397 4.0217 0.91276-2.6109 2.182-5.7102 0.3283-7.932 0.69965 1.4755 0.09798 4.4636-0.42959 6.0267-0.42961 1.2118-1.9816 2.8979-2.5725 3.3358 1.3169-1.5756 2.7659-3.31 2.7069-5.3954 0.55889-1.9106-0.70491-5.1142-3.0272-4.5658-1.0129 0.0956-3.4269 2.0908-3.4554 1.7915 1.0397-1.6217 3.2371-2.8233 5.0618-2.6442-0.69423-1.545-3.1889-0.0781-4.0742 0.84006z"/>
+		</g>
+		<text x="-696.14355" y="-27.217051" dominant-baseline="auto" fill="#000000" font-family="'Open Sans Condensed'" font-size="20.244px" letter-spacing="0px" stroke-width="1.5183" text-align="center" text-anchor="middle" word-spacing="0px" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;line-height:1.25;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal" xml:space="preserve"><tspan x="-696.14355" y="-27.217051" stroke-width="1.5183">lws_struct</tspan></text>
+	</g>
+</svg>
diff --git a/include/libwebsockets.h b/include/libwebsockets.h
index 2f60a9e8eb70cb7e3d9f6219e98de9a13123a108..31948abae14eb26e87b4381e1b57df4021d0383c 100644
--- a/include/libwebsockets.h
+++ b/include/libwebsockets.h
@@ -522,6 +522,7 @@ struct lws;
 #include <libwebsockets/lws-vfs.h>
 #include <libwebsockets/lws-lejp.h>
 #include <libwebsockets/lws-stats.h>
+#include <libwebsockets/lws-struct.h>
 #include <libwebsockets/lws-threadpool.h>
 #include <libwebsockets/lws-tokenize.h>
 #include <libwebsockets/lws-lwsac.h>
diff --git a/include/libwebsockets/lws-lejp.h b/include/libwebsockets/lws-lejp.h
index 8e502377d8fbb3a71f0365cf424165bb1a0c6ba8..81a93831310e2c68753c8c6d72ff2e6c5b2d9e56 100644
--- a/include/libwebsockets/lws-lejp.h
+++ b/include/libwebsockets/lws-lejp.h
@@ -173,6 +173,9 @@ LWS_EXTERN signed char _lejp_callback(struct lejp_ctx *ctx, char reason);
 
 typedef signed char (*lejp_callback)(struct lejp_ctx *ctx, char reason);
 
+#ifndef LEJP_MAX_PARSING_STACK_DEPTH
+#define LEJP_MAX_PARSING_STACK_DEPTH 5
+#endif
 #ifndef LEJP_MAX_DEPTH
 #define LEJP_MAX_DEPTH 12
 #endif
@@ -201,25 +204,36 @@ struct _lejp_stack {
 	char b; /* user bitfield */
 };
 
+struct _lejp_parsing_stack {
+	void *user;	/* private to the stack level */
+	signed char (*callback)(struct lejp_ctx *ctx, char reason);
+	const char * const *paths;
+	uint8_t count_paths;
+	uint8_t ppos;
+	uint8_t path_match;
+};
+
 struct lejp_ctx {
 
 	/* sorted by type for most compact alignment
 	 *
 	 * pointers
 	 */
-
-	signed char (*callback)(struct lejp_ctx *ctx, char reason);
 	void *user;
-	const char * const *paths;
 
 	/* arrays */
 
+	struct _lejp_parsing_stack pst[LEJP_MAX_PARSING_STACK_DEPTH];
 	struct _lejp_stack st[LEJP_MAX_DEPTH];
 	uint16_t i[LEJP_MAX_INDEX_DEPTH]; /* index array */
 	uint16_t wild[LEJP_MAX_INDEX_DEPTH]; /* index array */
 	char path[LEJP_MAX_PATH];
 	char buf[LEJP_STRING_CHUNK + 1];
 
+	/* size_t */
+
+	size_t path_stride; /* 0 means default ptr size, else stride */
+
 	/* int */
 
 	uint32_t line;
@@ -235,11 +249,11 @@ struct lejp_ctx {
 	uint8_t f;
 	uint8_t sp; /* stack head */
 	uint8_t ipos; /* index stack depth */
-	uint8_t ppos;
 	uint8_t count_paths;
 	uint8_t path_match;
 	uint8_t path_match_len;
 	uint8_t wildcount;
+	uint8_t pst_sp; /* parsing stack head */
 };
 
 LWS_VISIBLE LWS_EXTERN void
@@ -257,6 +271,21 @@ LWS_VISIBLE LWS_EXTERN void
 lejp_change_callback(struct lejp_ctx *ctx,
 		     signed char (*callback)(struct lejp_ctx *ctx, char reason));
 
+/*
+ * push the current paths / paths_count and lejp_cb to a stack in the ctx, and
+ * start using the new ones
+ */
+LWS_VISIBLE LWS_EXTERN int
+lejp_parser_push(struct lejp_ctx *ctx, void *user, const char * const *paths,
+		 unsigned char paths_count, lejp_callback lejp_cb);
+
+/*
+ * pop the previously used paths / paths_count and lejp_cb, and continue
+ * parsing using those as before
+ */
+LWS_VISIBLE LWS_EXTERN int
+lejp_parser_pop(struct lejp_ctx *ctx);
+
 /* exported for use when reevaluating a path for use with a subcontext */
 LWS_VISIBLE LWS_EXTERN void
 lejp_check_path_match(struct lejp_ctx *ctx);
diff --git a/include/libwebsockets/lws-misc.h b/include/libwebsockets/lws-misc.h
index 7ed1490dd62740286015d3f2b93b5cff6c8c9f1d..971bfa04228e557711abf8c6f219142bdc667e4e 100644
--- a/include/libwebsockets/lws-misc.h
+++ b/include/libwebsockets/lws-misc.h
@@ -241,18 +241,19 @@ lws_dll_foreach_safe(struct lws_dll *phead, void *user,
 struct lws_dll2;
 struct lws_dll2_owner;
 
-struct lws_dll2 {
+typedef struct lws_dll2 {
 	struct lws_dll2		*prev;
 	struct lws_dll2		*next;
 	struct lws_dll2_owner	*owner;
-};
+} lws_dll2_t;
 
-struct lws_dll2_owner {
+typedef struct lws_dll2_owner {
 	struct lws_dll2		*tail;
 	struct lws_dll2		*head;
 
 	uint32_t		count;
-};
+} lws_dll2_owner_t;
+
 static LWS_INLINE int
 lws_dll2_is_detached(const struct lws_dll2 *d) { return !d->owner; }
 
@@ -278,6 +279,12 @@ LWS_VISIBLE LWS_EXTERN int
 lws_dll2_foreach_safe(struct lws_dll2_owner *owner, void *user,
 		      int (*cb)(struct lws_dll2 *d, void *user));
 
+LWS_VISIBLE LWS_EXTERN void
+lws_dll2_clear(struct lws_dll2 *d);
+
+LWS_VISIBLE LWS_EXTERN void
+lws_dll2_owner_clear(struct lws_dll2_owner *d);
+
 /*
  * these are safe against the current container object getting deleted,
  * since the hold his next in a temp and go to that next.  ___tmp is
@@ -332,6 +339,7 @@ lws_buflist_append_segment(struct lws_buflist **head, const uint8_t *buf,
  */
 LWS_VISIBLE LWS_EXTERN size_t
 lws_buflist_next_segment_len(struct lws_buflist **head, uint8_t **buf);
+
 /**
  * lws_buflist_use_segment(): remove len bytes from the current segment
  *
@@ -349,6 +357,7 @@ lws_buflist_next_segment_len(struct lws_buflist **head, uint8_t **buf);
  */
 LWS_VISIBLE LWS_EXTERN int
 lws_buflist_use_segment(struct lws_buflist **head, size_t len);
+
 /**
  * lws_buflist_destroy_all_segments(): free all segments on the list
  *
diff --git a/include/libwebsockets/lws-struct.h b/include/libwebsockets/lws-struct.h
new file mode 100644
index 0000000000000000000000000000000000000000..0d02f59e5141b929c07fd0f5ad41d3491d208645
--- /dev/null
+++ b/include/libwebsockets/lws-struct.h
@@ -0,0 +1,258 @@
+/*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010-2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ *
+ * included from libwebsockets.h
+ */
+
+#if defined(LWS_WITH_STRUCT_SQLITE3)
+#include <sqlite3.h>
+#endif
+
+typedef enum {
+	LSMT_SIGNED,
+	LSMT_UNSIGNED,
+	LSMT_BOOLEAN,
+	LSMT_STRING_CHAR_ARRAY,
+	LSMT_STRING_PTR,
+	LSMT_LIST,
+	LSMT_CHILD_PTR,
+	LSMT_SCHEMA,
+
+} lws_struct_map_type_eum;
+
+typedef struct lejp_collation {
+	struct lws_dll2 chunks;
+	int len;
+	char buf[LEJP_STRING_CHUNK + 1];
+} lejp_collation_t;
+
+typedef struct lws_struct_map {
+	const char *colname;
+	const struct lws_struct_map *child_map;
+	lejp_callback lejp_cb;
+	size_t ofs;			/* child dll2; points to dll2_owner */
+	size_t aux;
+	size_t ofs_clist;
+	size_t child_map_size;
+	lws_struct_map_type_eum type;
+} lws_struct_map_t;
+
+typedef int (*lws_struct_args_cb)(void *obj, void *cb_arg);
+
+typedef struct lws_struct_args {
+	const lws_struct_map_t *map_st[LEJP_MAX_PARSING_STACK_DEPTH];
+	lws_struct_args_cb cb;
+	struct lwsac *ac;
+	void *cb_arg;
+	void *dest;
+
+	size_t dest_len;
+	size_t toplevel_dll2_ofs;
+	size_t map_entries_st[LEJP_MAX_PARSING_STACK_DEPTH];
+	size_t ac_block_size;
+	int subtype;
+
+	/*
+	 * temp ac used to collate unknown possibly huge strings before final
+	 * allocation and copy
+	 */
+	struct lwsac *ac_chunks;
+	struct lws_dll2_owner chunks_owner;
+	size_t chunks_length;
+} lws_struct_args_t;
+
+#define LSM_SIGNED(type, name, qname) \
+	{ \
+	  qname, \
+	  NULL, \
+	  NULL, \
+	  offsetof(type, name), \
+	  sizeof ((type *)0)->name, \
+	  0, \
+	  0, \
+	  LSMT_SIGNED \
+	}
+
+#define LSM_UNSIGNED(type, name, qname) \
+	{ \
+	  qname, \
+	  NULL, \
+	  NULL, \
+	  offsetof(type, name), \
+	  sizeof ((type *)0)->name, \
+	  0, \
+	  0, \
+	  LSMT_UNSIGNED \
+	}
+
+#define LSM_BOOLEAN(type, name, qname) \
+	{ \
+	  qname, \
+	  NULL, \
+	  NULL, \
+	  offsetof(type, name), \
+	  sizeof ((type *)0)->name, \
+	  0, \
+	  0, \
+	  LSMT_BOOLEAN \
+	}
+
+#define LSM_CARRAY(type, name, qname) \
+	{ \
+	  qname, \
+	  NULL, \
+	  NULL, \
+	  offsetof(type, name), \
+	  sizeof (((type *)0)->name), \
+	  0, \
+	  0, \
+	  LSMT_STRING_CHAR_ARRAY \
+	}
+
+#define LSM_STRING_PTR(type, name, qname) \
+	{ \
+	  qname, \
+	  NULL, \
+	  NULL, \
+	  offsetof(type, name), \
+	  sizeof (((type *)0)->name), \
+	  0, \
+	  0, \
+	  LSMT_STRING_PTR \
+	}
+
+#define LSM_LIST(ptype, pname, ctype, cname, lejp_cb, cmap, qname) \
+	{ \
+	  qname, \
+	  cmap, \
+	  lejp_cb, \
+	  offsetof(ptype, pname), \
+	  sizeof (ctype), \
+	  offsetof(ctype, cname), \
+	  LWS_ARRAY_SIZE(cmap), \
+	  LSMT_LIST \
+	}
+
+#define LSM_CHILD_PTR(ptype, pname, ctype, lejp_cb, cmap, qname) \
+	{ \
+	  qname, \
+	  cmap, \
+	  lejp_cb, \
+	  offsetof(ptype, pname), \
+	  sizeof (ctype), \
+	  0, \
+	  LWS_ARRAY_SIZE(cmap), \
+	  LSMT_CHILD_PTR \
+	}
+
+#define LSM_SCHEMA(ctype, lejp_cb, map, schema_name) \
+	{ \
+	  schema_name, \
+	  map, \
+	  lejp_cb, \
+	  0, \
+	  sizeof (ctype), \
+	  0, \
+	  LWS_ARRAY_SIZE(map), \
+	  LSMT_SCHEMA \
+	}
+
+#define LSM_SCHEMA_DLL2(ctype, cdll2mem, lejp_cb, map, schema_name) \
+	{ \
+	  schema_name, \
+	  map, \
+	  lejp_cb, \
+	  offsetof(ctype, cdll2mem), \
+	  sizeof (ctype), \
+	  0, \
+	  LWS_ARRAY_SIZE(map), \
+	  LSMT_SCHEMA \
+	}
+
+typedef struct lws_struct_serialize_st {
+	const struct lws_dll2 *dllpos;
+	const lws_struct_map_t *map;
+	const char *obj;
+	size_t map_entries;
+	size_t map_entry;
+	size_t size;
+	char subsequent;
+	char idt;
+} lws_struct_serialize_st_t;
+
+enum {
+	LSSERJ_FLAG_PRETTY = 1
+};
+
+typedef struct lws_struct_serialize {
+	lws_struct_serialize_st_t st[LEJP_MAX_PARSING_STACK_DEPTH];
+
+	size_t offset;
+	size_t remaining;
+
+	int sp;
+	int flags;
+} lws_struct_serialize_t;
+
+typedef enum {
+	LSJS_RESULT_CONTINUE,
+	LSJS_RESULT_FINISH,
+	LSJS_RESULT_ERROR
+} lws_struct_json_serialize_result_t;
+
+LWS_VISIBLE LWS_EXTERN int
+lws_struct_json_init_parse(struct lejp_ctx *ctx, lejp_callback cb,
+			   void *user);
+
+LWS_VISIBLE LWS_EXTERN signed char
+lws_struct_schema_only_lejp_cb(struct lejp_ctx *ctx, char reason);
+
+LWS_VISIBLE LWS_EXTERN signed char
+lws_struct_default_lejp_cb(struct lejp_ctx *ctx, char reason);
+
+LWS_VISIBLE LWS_EXTERN lws_struct_serialize_t *
+lws_struct_json_serialize_create(const lws_struct_map_t *map,
+				 size_t map_entries, int flags, void *ptoplevel);
+
+LWS_VISIBLE LWS_EXTERN void
+lws_struct_json_serialize_destroy(lws_struct_serialize_t **pjs);
+
+LWS_VISIBLE LWS_EXTERN lws_struct_json_serialize_result_t
+lws_struct_json_serialize(lws_struct_serialize_t *js, uint8_t *buf,
+			  size_t len, size_t *written);
+
+#if defined(LWS_WITH_STRUCT_SQLITE3)
+
+LWS_VISIBLE LWS_EXTERN int
+lws_struct_sq3_deserialize(sqlite3 *pdb, const lws_struct_map_t *schema,
+			   lws_dll2_owner_t *o, struct lwsac **ac,
+			   uint64_t start, int limit);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_struct_sq3_create_table(sqlite3 *pdb, const lws_struct_map_t *schema);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_struct_sq3_open(struct lws_context *context, const char *sqlite3_path,
+		    sqlite3 **pdb);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_struct_sq3_close(sqlite3 **pdb);
+
+#endif
diff --git a/lib/core/buflist.c b/lib/core/buflist.c
new file mode 100644
index 0000000000000000000000000000000000000000..6d8190fac821c5b8bbe5ca3641b55810b2068855
--- /dev/null
+++ b/lib/core/buflist.c
@@ -0,0 +1,170 @@
+/*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010-2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+
+#include "core/private.h"
+
+#ifdef LWS_HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+
+/* lws_buflist */
+
+int
+lws_buflist_append_segment(struct lws_buflist **head, const uint8_t *buf,
+			   size_t len)
+{
+	struct lws_buflist *nbuf;
+	int first = !*head;
+	void *p = *head;
+	int sanity = 1024;
+
+	assert(buf);
+	assert(len);
+
+	/* append at the tail */
+	while (*head) {
+		if (!--sanity) {
+			lwsl_err("%s: buflist reached sanity limit\n", __func__);
+			return -1;
+		}
+		if (*head == (*head)->next) {
+			lwsl_err("%s: corrupt list points to self\n", __func__);
+			return -1;
+		}
+		head = &((*head)->next);
+	}
+
+	lwsl_info("%s: len %u first %d %p\n", __func__, (uint32_t)len, first, p);
+
+	nbuf = (struct lws_buflist *)lws_malloc(sizeof(**head) + len, __func__);
+	if (!nbuf) {
+		lwsl_err("%s: OOM\n", __func__);
+		return -1;
+	}
+
+	nbuf->len = len;
+	nbuf->pos = 0;
+	nbuf->next = NULL;
+
+	p = (void *)nbuf->buf;
+	memcpy(p, buf, len);
+
+	*head = nbuf;
+
+	return first; /* returns 1 if first segment just created */
+}
+
+static int
+lws_buflist_destroy_segment(struct lws_buflist **head)
+{
+	struct lws_buflist *old = *head;
+
+	assert(*head);
+	*head = old->next;
+	old->next = NULL;
+	lws_free(old);
+
+	return !*head; /* returns 1 if last segment just destroyed */
+}
+
+void
+lws_buflist_destroy_all_segments(struct lws_buflist **head)
+{
+	struct lws_buflist *p = *head, *p1;
+
+	while (p) {
+		p1 = p->next;
+		p->next = NULL;
+		lws_free(p);
+		p = p1;
+	}
+
+	*head = NULL;
+}
+
+size_t
+lws_buflist_next_segment_len(struct lws_buflist **head, uint8_t **buf)
+{
+	if (!*head) {
+		if (buf)
+			*buf = NULL;
+
+		return 0;
+	}
+
+	if (!(*head)->len && (*head)->next)
+		lws_buflist_destroy_segment(head);
+
+	if (!*head) {
+		if (buf)
+			*buf = NULL;
+
+		return 0;
+	}
+
+	assert((*head)->pos < (*head)->len);
+
+	if (buf)
+		*buf = (*head)->buf + (*head)->pos;
+
+	return (*head)->len - (*head)->pos;
+}
+
+int
+lws_buflist_use_segment(struct lws_buflist **head, size_t len)
+{
+	assert(*head);
+	assert(len);
+	assert((*head)->pos + len <= (*head)->len);
+
+	(*head)->pos += len;
+	if ((*head)->pos == (*head)->len)
+		lws_buflist_destroy_segment(head);
+
+	if (!*head)
+		return 0;
+
+	return (int)((*head)->len - (*head)->pos);
+}
+
+void
+lws_buflist_describe(struct lws_buflist **head, void *id)
+{
+	struct lws_buflist *old;
+	int n = 0;
+
+	if (*head == NULL)
+		lwsl_notice("%p: buflist empty\n", id);
+
+	while (*head) {
+		lwsl_notice("%p: %d: %llu / %llu (%llu left)\n", id, n,
+			    (unsigned long long)(*head)->pos,
+			    (unsigned long long)(*head)->len,
+			    (unsigned long long)(*head)->len - (*head)->pos);
+		old = *head;
+		head = &((*head)->next);
+		if (*head == old) {
+			lwsl_err("%s: next points to self\n", __func__);
+			break;
+		}
+		n++;
+	}
+}
diff --git a/lib/core/libwebsockets.c b/lib/core/libwebsockets.c
index 6582566b562b4c5541b6a8f56f8372040c9cc189..9791ae19475ead01847e39f9b566e3c0b9925771 100644
--- a/lib/core/libwebsockets.c
+++ b/lib/core/libwebsockets.c
@@ -94,321 +94,6 @@ int lws_open(const char *__file, int __oflag, ...)
 #endif
 
 
-void
-lws_dll_add_head(struct lws_dll *d, struct lws_dll *phead)
-{
-	if (!lws_dll_is_detached(d, phead)) {
-		assert(0); /* only wholly detached things can be added */
-		return;
-	}
-
-	/* our next guy is current first guy, if any */
-	if (phead->next != d)
-		d->next = phead->next;
-
-	/* if there is a next guy, set his prev ptr to our next ptr */
-	if (d->next)
-		d->next->prev = d;
-	/* there is nobody previous to us, we are the head */
-	d->prev = NULL;
-
-	/* set the first guy to be us */
-	phead->next = d;
-
-	/* if there was nothing on the list before, we are also now the tail */
-	if (!phead->prev)
-		phead->prev = d;
-
-	assert(d->prev != d);
-	assert(d->next != d);
-}
-
-void
-lws_dll_add_tail(struct lws_dll *d, struct lws_dll *phead)
-{
-	if (!lws_dll_is_detached(d, phead)) {
-		assert(0); /* only wholly detached things can be added */
-		return;
-	}
-
-	/* our previous guy is current last guy */
-	d->prev = phead->prev;
-	/* if there is a prev guy, set his next ptr to our prev ptr */
-	if (d->prev)
-		d->prev->next = d;
-	/* our next ptr is NULL */
-	d->next = NULL;
-	/* set the last guy to be us */
-	phead->prev = d;
-
-	/* list head is also us if we're the first */
-	if (!phead->next)
-		phead->next = d;
-
-	assert(d->prev != d);
-	assert(d->next != d);
-}
-
-void
-lws_dll_insert(struct lws_dll *n, struct lws_dll *target,
-	       struct lws_dll *phead, int before)
-{
-	if (!lws_dll_is_detached(n, phead)) {
-		assert(0); /* only wholly detached things can be inserted */
-		return;
-	}
-	if (!target) {
-		/*
-		 * the case where there's no target identified degenerates to
-		 * a simple add at head or tail
-		 */
-		if (before) {
-			lws_dll_add_head(n, phead);
-			return;
-		}
-		lws_dll_add_tail(n, phead);
-		return;
-	}
-
-	/*
-	 * in the case there's a target "cursor", we have to do the work to
-	 * stitch the new guy in appropriately
-	 */
-
-	if (before) {
-		/*
-		 *  we go before dd
-		 *  DDp <-> DD <-> DDn   -->   DDp <-> us <-> DD <-> DDn
-		 */
-		/* we point forward to dd */
-		n->next = target;
-		/* we point back to what dd used to point back to */
-		n->prev = target->prev;
-		/* DDp points forward to us now */
-		if (target->prev)
-			target->prev->next = n;
-		/* DD points back to us now */
-		target->prev = n;
-
-		/* if target was the head, we are now the head */
-		if (phead->next == target)
-			phead->next = n;
-
-		/* since we are before another guy, we cannot become the tail */
-
-	} else {
-		/*
-		 *  we go after dd
-		 *  DDp <-> DD <-> DDn   -->   DDp <-> DD <-> us <-> DDn
-		 */
-		/* we point forward to what dd used to point forward to */
-		n->next = target->next;
-		/* we point back to dd */
-		n->prev = target;
-		/* DDn points back to us */
-		if (target->next)
-			target->next->prev = n;
-		/* DD points forward to us */
-		target->next = n;
-
-		/* if target was the tail, we are now the tail */
-		if (phead->prev == target)
-			phead->prev = n;
-
-		/* since we go after another guy, we cannot become the head */
-	}
-}
-
-/* situation is:
- *
- *  HEAD: struct lws_dll * = &entry1
- *
- *  Entry 1: struct lws_dll  .pprev = &HEAD , .next = Entry 2
- *  Entry 2: struct lws_dll  .pprev = &entry1 , .next = &entry2
- *  Entry 3: struct lws_dll  .pprev = &entry2 , .next = NULL
- *
- *  Delete Entry1:
- *
- *   - HEAD = &entry2
- *   - Entry2: .pprev = &HEAD, .next = &entry3
- *   - Entry3: .pprev = &entry2, .next = NULL
- *
- *  Delete Entry2:
- *
- *   - HEAD = &entry1
- *   - Entry1: .pprev = &HEAD, .next = &entry3
- *   - Entry3: .pprev = &entry1, .next = NULL
- *
- *  Delete Entry3:
- *
- *   - HEAD = &entry1
- *   - Entry1: .pprev = &HEAD, .next = &entry2
- *   - Entry2: .pprev = &entry1, .next = NULL
- *
- */
-
-void
-lws_dll_remove(struct lws_dll *d)
-{
-	if (!d->prev && !d->next)
-		return;
-
-	/*
-	 *  remove us
-	 *
-	 *  USp <-> us <-> USn  -->  USp <-> USn
-	 */
-
-	/* if we have a next guy, set his prev to our prev */
-	if (d->next)
-		d->next->prev = d->prev;
-
-	/* set our prev guy to our next guy instead of us */
-	if (d->prev)
-		d->prev->next = d->next;
-
-	/* we're out of the list, we should not point anywhere any more */
-	d->prev = NULL;
-	d->next = NULL;
-}
-
-void
-lws_dll_remove_track_tail(struct lws_dll *d, struct lws_dll *phead)
-{
-	if (lws_dll_is_detached(d, phead)) {
-		assert(phead->prev != d);
-		assert(phead->next != d);
-		return;
-	}
-
-	/* if we have a next guy, set his prev to our prev */
-	if (d->next)
-		d->next->prev = d->prev;
-
-	/* if we have a previous guy, set his next to our next */
-	if (d->prev)
-		d->prev->next = d->next;
-
-	if (phead->prev == d)
-		phead->prev = d->prev;
-
-	if (phead->next == d)
-		phead->next = d->next;
-
-	/* we're out of the list, we should not point anywhere any more */
-	d->prev = NULL;
-	d->next = NULL;
-}
-
-
-int
-lws_dll_foreach_safe(struct lws_dll *phead, void *user,
-		     int (*cb)(struct lws_dll *d, void *user))
-{
-	lws_start_foreach_dll_safe(struct lws_dll *, p, tp, phead->next) {
-		if (cb(p, user))
-			return 1;
-	} lws_end_foreach_dll_safe(p, tp);
-
-	return 0;
-}
-
-int
-lws_dll2_foreach_safe(struct lws_dll2_owner *owner, void *user,
-		      int (*cb)(struct lws_dll2 *d, void *user))
-{
-	lws_start_foreach_dll_safe(struct lws_dll2 *, p, tp, owner->head) {
-		if (cb(p, user))
-			return 1;
-	} lws_end_foreach_dll_safe(p, tp);
-
-	return 0;
-}
-
-void
-lws_dll2_add_head(struct lws_dll2 *d, struct lws_dll2_owner *owner)
-{
-	if (!lws_dll2_is_detached(d)) {
-		assert(0); /* only wholly detached things can be added */
-		return;
-	}
-
-	/* our next guy is current first guy, if any */
-	if (owner->head != d)
-		d->next = owner->head;
-
-	/* if there is a next guy, set his prev ptr to our next ptr */
-	if (d->next)
-		d->next->prev = d;
-	/* there is nobody previous to us, we are the head */
-	d->prev = NULL;
-
-	/* set the first guy to be us */
-	owner->head = d;
-
-	if (!owner->tail)
-		owner->tail = d;
-
-	d->owner = owner;
-	owner->count++;
-}
-
-void
-lws_dll2_add_tail(struct lws_dll2 *d, struct lws_dll2_owner *owner)
-{
-	if (!lws_dll2_is_detached(d)) {
-		assert(0); /* only wholly detached things can be added */
-		return;
-	}
-
-	/* our previous guy is current last guy */
-	d->prev = owner->tail;
-	/* if there is a prev guy, set his next ptr to our prev ptr */
-	if (d->prev)
-		d->prev->next = d;
-	/* our next ptr is NULL */
-	d->next = NULL;
-	/* set the last guy to be us */
-	owner->tail = d;
-
-	/* list head is also us if we're the first */
-	if (!owner->head)
-		owner->head = d;
-
-	d->owner = owner;
-	owner->count++;
-}
-
-void
-lws_dll2_remove(struct lws_dll2 *d)
-{
-	if (lws_dll2_is_detached(d))
-		return;
-
-	/* if we have a next guy, set his prev to our prev */
-	if (d->next)
-		d->next->prev = d->prev;
-
-	/* if we have a previous guy, set his next to our next */
-	if (d->prev)
-		d->prev->next = d->next;
-
-	/* if we have phead, track the tail and head if it points to us... */
-
-	if (d->owner->tail == d)
-		d->owner->tail = d->prev;
-
-	if (d->owner->head == d)
-		d->owner->head = d->next;
-
-	d->owner->count--;
-
-	/* we're out of the list, we should not point anywhere any more */
-	d->owner = NULL;
-	d->prev = NULL;
-	d->next = NULL;
-}
-
 #if !(defined(LWS_PLAT_OPTEE) && !defined(LWS_WITH_NETWORK))
 
 LWS_VISIBLE lws_usec_t
@@ -441,151 +126,6 @@ lws_pthread_self_to_tsi(struct lws_context *context)
 #endif
 }
 
-
-/* lws_buflist */
-
-int
-lws_buflist_append_segment(struct lws_buflist **head, const uint8_t *buf,
-			   size_t len)
-{
-	struct lws_buflist *nbuf;
-	int first = !*head;
-	void *p = *head;
-	int sanity = 1024;
-
-	assert(buf);
-	assert(len);
-
-	/* append at the tail */
-	while (*head) {
-		if (!--sanity) {
-			lwsl_err("%s: buflist reached sanity limit\n", __func__);
-			return -1;
-		}
-		if (*head == (*head)->next) {
-			lwsl_err("%s: corrupt list points to self\n", __func__);
-			return -1;
-		}
-		head = &((*head)->next);
-	}
-
-	lwsl_info("%s: len %u first %d %p\n", __func__, (uint32_t)len, first, p);
-
-	nbuf = (struct lws_buflist *)lws_malloc(sizeof(**head) + len, __func__);
-	if (!nbuf) {
-		lwsl_err("%s: OOM\n", __func__);
-		return -1;
-	}
-
-	nbuf->len = len;
-	nbuf->pos = 0;
-	nbuf->next = NULL;
-
-	p = (void *)nbuf->buf;
-	memcpy(p, buf, len);
-
-	*head = nbuf;
-
-	return first; /* returns 1 if first segment just created */
-}
-
-static int
-lws_buflist_destroy_segment(struct lws_buflist **head)
-{
-	struct lws_buflist *old = *head;
-
-	assert(*head);
-	*head = old->next;
-	old->next = NULL;
-	lws_free(old);
-
-	return !*head; /* returns 1 if last segment just destroyed */
-}
-
-void
-lws_buflist_destroy_all_segments(struct lws_buflist **head)
-{
-	struct lws_buflist *p = *head, *p1;
-
-	while (p) {
-		p1 = p->next;
-		p->next = NULL;
-		lws_free(p);
-		p = p1;
-	}
-
-	*head = NULL;
-}
-
-size_t
-lws_buflist_next_segment_len(struct lws_buflist **head, uint8_t **buf)
-{
-	if (!*head) {
-		if (buf)
-			*buf = NULL;
-
-		return 0;
-	}
-
-	if (!(*head)->len && (*head)->next)
-		lws_buflist_destroy_segment(head);
-
-	if (!*head) {
-		if (buf)
-			*buf = NULL;
-
-		return 0;
-	}
-
-	assert((*head)->pos < (*head)->len);
-
-	if (buf)
-		*buf = (*head)->buf + (*head)->pos;
-
-	return (*head)->len - (*head)->pos;
-}
-
-int
-lws_buflist_use_segment(struct lws_buflist **head, size_t len)
-{
-	assert(*head);
-	assert(len);
-	assert((*head)->pos + len <= (*head)->len);
-
-	(*head)->pos += len;
-	if ((*head)->pos == (*head)->len)
-		lws_buflist_destroy_segment(head);
-
-	if (!*head)
-		return 0;
-
-	return (int)((*head)->len - (*head)->pos);
-}
-
-void
-lws_buflist_describe(struct lws_buflist **head, void *id)
-{
-	struct lws_buflist *old;
-	int n = 0;
-
-	if (*head == NULL)
-		lwsl_notice("%p: buflist empty\n", id);
-
-	while (*head) {
-		lwsl_notice("%p: %d: %llu / %llu (%llu left)\n", id, n,
-			    (unsigned long long)(*head)->pos,
-			    (unsigned long long)(*head)->len,
-			    (unsigned long long)(*head)->len - (*head)->pos);
-		old = *head;
-		head = &((*head)->next);
-		if (*head == old) {
-			lwsl_err("%s: next points to self\n", __func__);
-			break;
-		}
-		n++;
-	}
-}
-
 LWS_EXTERN void *
 lws_context_user(struct lws_context *context)
 {
diff --git a/lib/core/lws_dll.c b/lib/core/lws_dll.c
new file mode 100644
index 0000000000000000000000000000000000000000..bbb9c2b26c03b27c8a2d980f58ffcf51521e56a5
--- /dev/null
+++ b/lib/core/lws_dll.c
@@ -0,0 +1,246 @@
+/*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010-2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+
+#include "core/private.h"
+
+#ifdef LWS_HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+
+
+void
+lws_dll_add_head(struct lws_dll *d, struct lws_dll *phead)
+{
+	if (!lws_dll_is_detached(d, phead)) {
+		assert(0); /* only wholly detached things can be added */
+		return;
+	}
+
+	/* our next guy is current first guy, if any */
+	if (phead->next != d)
+		d->next = phead->next;
+
+	/* if there is a next guy, set his prev ptr to our next ptr */
+	if (d->next)
+		d->next->prev = d;
+	/* there is nobody previous to us, we are the head */
+	d->prev = NULL;
+
+	/* set the first guy to be us */
+	phead->next = d;
+
+	/* if there was nothing on the list before, we are also now the tail */
+	if (!phead->prev)
+		phead->prev = d;
+
+	assert(d->prev != d);
+	assert(d->next != d);
+}
+
+void
+lws_dll_add_tail(struct lws_dll *d, struct lws_dll *phead)
+{
+	if (!lws_dll_is_detached(d, phead)) {
+		assert(0); /* only wholly detached things can be added */
+		return;
+	}
+
+	/* our previous guy is current last guy */
+	d->prev = phead->prev;
+	/* if there is a prev guy, set his next ptr to our prev ptr */
+	if (d->prev)
+		d->prev->next = d;
+	/* our next ptr is NULL */
+	d->next = NULL;
+	/* set the last guy to be us */
+	phead->prev = d;
+
+	/* list head is also us if we're the first */
+	if (!phead->next)
+		phead->next = d;
+
+	assert(d->prev != d);
+	assert(d->next != d);
+}
+
+void
+lws_dll_insert(struct lws_dll *n, struct lws_dll *target,
+	       struct lws_dll *phead, int before)
+{
+	if (!lws_dll_is_detached(n, phead)) {
+		assert(0); /* only wholly detached things can be inserted */
+		return;
+	}
+	if (!target) {
+		/*
+		 * the case where there's no target identified degenerates to
+		 * a simple add at head or tail
+		 */
+		if (before) {
+			lws_dll_add_head(n, phead);
+			return;
+		}
+		lws_dll_add_tail(n, phead);
+		return;
+	}
+
+	/*
+	 * in the case there's a target "cursor", we have to do the work to
+	 * stitch the new guy in appropriately
+	 */
+
+	if (before) {
+		/*
+		 *  we go before dd
+		 *  DDp <-> DD <-> DDn   -->   DDp <-> us <-> DD <-> DDn
+		 */
+		/* we point forward to dd */
+		n->next = target;
+		/* we point back to what dd used to point back to */
+		n->prev = target->prev;
+		/* DDp points forward to us now */
+		if (target->prev)
+			target->prev->next = n;
+		/* DD points back to us now */
+		target->prev = n;
+
+		/* if target was the head, we are now the head */
+		if (phead->next == target)
+			phead->next = n;
+
+		/* since we are before another guy, we cannot become the tail */
+
+	} else {
+		/*
+		 *  we go after dd
+		 *  DDp <-> DD <-> DDn   -->   DDp <-> DD <-> us <-> DDn
+		 */
+		/* we point forward to what dd used to point forward to */
+		n->next = target->next;
+		/* we point back to dd */
+		n->prev = target;
+		/* DDn points back to us */
+		if (target->next)
+			target->next->prev = n;
+		/* DD points forward to us */
+		target->next = n;
+
+		/* if target was the tail, we are now the tail */
+		if (phead->prev == target)
+			phead->prev = n;
+
+		/* since we go after another guy, we cannot become the head */
+	}
+}
+
+/* situation is:
+ *
+ *  HEAD: struct lws_dll * = &entry1
+ *
+ *  Entry 1: struct lws_dll  .pprev = &HEAD , .next = Entry 2
+ *  Entry 2: struct lws_dll  .pprev = &entry1 , .next = &entry2
+ *  Entry 3: struct lws_dll  .pprev = &entry2 , .next = NULL
+ *
+ *  Delete Entry1:
+ *
+ *   - HEAD = &entry2
+ *   - Entry2: .pprev = &HEAD, .next = &entry3
+ *   - Entry3: .pprev = &entry2, .next = NULL
+ *
+ *  Delete Entry2:
+ *
+ *   - HEAD = &entry1
+ *   - Entry1: .pprev = &HEAD, .next = &entry3
+ *   - Entry3: .pprev = &entry1, .next = NULL
+ *
+ *  Delete Entry3:
+ *
+ *   - HEAD = &entry1
+ *   - Entry1: .pprev = &HEAD, .next = &entry2
+ *   - Entry2: .pprev = &entry1, .next = NULL
+ *
+ */
+
+void
+lws_dll_remove(struct lws_dll *d)
+{
+	if (!d->prev && !d->next)
+		return;
+
+	/*
+	 *  remove us
+	 *
+	 *  USp <-> us <-> USn  -->  USp <-> USn
+	 */
+
+	/* if we have a next guy, set his prev to our prev */
+	if (d->next)
+		d->next->prev = d->prev;
+
+	/* set our prev guy to our next guy instead of us */
+	if (d->prev)
+		d->prev->next = d->next;
+
+	/* we're out of the list, we should not point anywhere any more */
+	d->prev = NULL;
+	d->next = NULL;
+}
+
+void
+lws_dll_remove_track_tail(struct lws_dll *d, struct lws_dll *phead)
+{
+	if (lws_dll_is_detached(d, phead)) {
+		assert(phead->prev != d);
+		assert(phead->next != d);
+		return;
+	}
+
+	/* if we have a next guy, set his prev to our prev */
+	if (d->next)
+		d->next->prev = d->prev;
+
+	/* if we have a previous guy, set his next to our next */
+	if (d->prev)
+		d->prev->next = d->next;
+
+	if (phead->prev == d)
+		phead->prev = d->prev;
+
+	if (phead->next == d)
+		phead->next = d->next;
+
+	/* we're out of the list, we should not point anywhere any more */
+	d->prev = NULL;
+	d->next = NULL;
+}
+
+
+int
+lws_dll_foreach_safe(struct lws_dll *phead, void *user,
+		     int (*cb)(struct lws_dll *d, void *user))
+{
+	lws_start_foreach_dll_safe(struct lws_dll *, p, tp, phead->next) {
+		if (cb(p, user))
+			return 1;
+	} lws_end_foreach_dll_safe(p, tp);
+
+	return 0;
+}
diff --git a/lib/core/lws_dll2.c b/lib/core/lws_dll2.c
new file mode 100644
index 0000000000000000000000000000000000000000..dad96bb38269b5acde8d045cab0a2108bf1b61c2
--- /dev/null
+++ b/lib/core/lws_dll2.c
@@ -0,0 +1,138 @@
+/*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010-2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+
+#include "core/private.h"
+
+#ifdef LWS_HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+
+int
+lws_dll2_foreach_safe(struct lws_dll2_owner *owner, void *user,
+		      int (*cb)(struct lws_dll2 *d, void *user))
+{
+	lws_start_foreach_dll_safe(struct lws_dll2 *, p, tp, owner->head) {
+		if (cb(p, user))
+			return 1;
+	} lws_end_foreach_dll_safe(p, tp);
+
+	return 0;
+}
+
+void
+lws_dll2_add_head(struct lws_dll2 *d, struct lws_dll2_owner *owner)
+{
+	if (!lws_dll2_is_detached(d)) {
+		assert(0); /* only wholly detached things can be added */
+		return;
+	}
+
+	/* our next guy is current first guy, if any */
+	if (owner->head != d)
+		d->next = owner->head;
+
+	/* if there is a next guy, set his prev ptr to our next ptr */
+	if (d->next)
+		d->next->prev = d;
+	/* there is nobody previous to us, we are the head */
+	d->prev = NULL;
+
+	/* set the first guy to be us */
+	owner->head = d;
+
+	if (!owner->tail)
+		owner->tail = d;
+
+	d->owner = owner;
+	owner->count++;
+}
+
+void
+lws_dll2_add_tail(struct lws_dll2 *d, struct lws_dll2_owner *owner)
+{
+	if (!lws_dll2_is_detached(d)) {
+		assert(0); /* only wholly detached things can be added */
+		return;
+	}
+
+	/* our previous guy is current last guy */
+	d->prev = owner->tail;
+	/* if there is a prev guy, set his next ptr to our prev ptr */
+	if (d->prev)
+		d->prev->next = d;
+	/* our next ptr is NULL */
+	d->next = NULL;
+	/* set the last guy to be us */
+	owner->tail = d;
+
+	/* list head is also us if we're the first */
+	if (!owner->head)
+		owner->head = d;
+
+	d->owner = owner;
+	owner->count++;
+}
+
+void
+lws_dll2_remove(struct lws_dll2 *d)
+{
+	if (lws_dll2_is_detached(d))
+		return;
+
+	/* if we have a next guy, set his prev to our prev */
+	if (d->next)
+		d->next->prev = d->prev;
+
+	/* if we have a previous guy, set his next to our next */
+	if (d->prev)
+		d->prev->next = d->next;
+
+	/* if we have phead, track the tail and head if it points to us... */
+
+	if (d->owner->tail == d)
+		d->owner->tail = d->prev;
+
+	if (d->owner->head == d)
+		d->owner->head = d->next;
+
+	d->owner->count--;
+
+	/* we're out of the list, we should not point anywhere any more */
+	d->owner = NULL;
+	d->prev = NULL;
+	d->next = NULL;
+}
+
+void
+lws_dll2_clear(struct lws_dll2 *d)
+{
+	d->owner = NULL;
+	d->prev = NULL;
+	d->next = NULL;
+}
+
+void
+lws_dll2_owner_clear(struct lws_dll2_owner *d)
+{
+	d->head = NULL;
+	d->tail = NULL;
+	d->count = 0;
+}
diff --git a/lib/jose/jws/jose.c b/lib/jose/jws/jose.c
index 2cb337be104fe24a189f640affa23ff0d4c0597f..627fd237a857ef9845a8515fa1f1cdd3c7e26f69 100644
--- a/lib/jose/jws/jose.c
+++ b/lib/jose/jws/jose.c
@@ -176,7 +176,8 @@ lws_jws_jose_cb(struct lejp_ctx *ctx, char reason)
 		lejp_check_path_match(&args->jwk_jctx);
 
 		if (args->jwk_jctx.path_match)
-			args->jwk_jctx.callback(&args->jwk_jctx, reason);
+			args->jwk_jctx.pst[args->jwk_jctx.pst_sp].
+				callback(&args->jwk_jctx, reason);
 	}
 
 	// lwsl_notice("%s: %s %d (%d)\n", __func__, ctx->path, reason, ctx->sp);
diff --git a/lib/misc/lejp.c b/lib/misc/lejp.c
index 370126b6df33ec83f5b21f6525ba2a831ad71222..9f974479ec628d3812406e0bf98a0a600f502078 100644
--- a/lib/misc/lejp.c
+++ b/lib/misc/lejp.c
@@ -74,15 +74,20 @@ lejp_construct(struct lejp_ctx *ctx,
 	ctx->st[0].b = 0;
 	ctx->sp = 0;
 	ctx->ipos = 0;
-	ctx->ppos = 0;
 	ctx->path_match = 0;
+	ctx->path_stride = 0;
 	ctx->path[0] = '\0';
-	ctx->callback = callback;
 	ctx->user = user;
-	ctx->paths = paths;
-	ctx->count_paths = count_paths;
 	ctx->line = 1;
-	ctx->callback(ctx, LEJPCB_CONSTRUCTED);
+
+	ctx->pst_sp = 0;
+	ctx->pst[0].callback = callback;
+	ctx->pst[0].paths = paths;
+	ctx->pst[0].count_paths = count_paths;
+	ctx->pst[0].user = NULL;
+	ctx->pst[0].ppos = 0;
+
+	ctx->pst[0].callback(ctx, LEJPCB_CONSTRUCTED);
 }
 
 /**
@@ -99,7 +104,7 @@ void
 lejp_destruct(struct lejp_ctx *ctx)
 {
 	/* no allocations... just let callback know what it happening */
-	ctx->callback(ctx, LEJPCB_DESTRUCTED);
+	ctx->pst[0].callback(ctx, LEJPCB_DESTRUCTED);
 }
 
 /**
@@ -128,23 +133,29 @@ void
 lejp_change_callback(struct lejp_ctx *ctx,
 		     signed char (*callback)(struct lejp_ctx *ctx, char reason))
 {
-	ctx->callback(ctx, LEJPCB_DESTRUCTED);
-	ctx->callback = callback;
-	ctx->callback(ctx, LEJPCB_CONSTRUCTED);
-	ctx->callback(ctx, LEJPCB_START);
+	ctx->pst[0].callback(ctx, LEJPCB_DESTRUCTED);
+	ctx->pst[0].callback = callback;
+	ctx->pst[0].callback(ctx, LEJPCB_CONSTRUCTED);
+	ctx->pst[0].callback(ctx, LEJPCB_START);
 }
 
 void
 lejp_check_path_match(struct lejp_ctx *ctx)
 {
 	const char *p, *q;
-	int n;
+	int n, s = sizeof(char *);
+
+	if (ctx->path_stride)
+		s = ctx->path_stride;
 
 	/* we only need to check if a match is not active */
-	for (n = 0; !ctx->path_match && n < ctx->count_paths; n++) {
+	for (n = 0; !ctx->path_match &&
+	     n < ctx->pst[ctx->pst_sp].count_paths; n++) {
 		ctx->wildcount = 0;
 		p = ctx->path;
-		q = ctx->paths[n];
+
+		q = *((char **)(((char *)ctx->pst[ctx->pst_sp].paths) + (n * s)));
+
 		while (*p && *q) {
 			if (*q != '*') {
 				if (*p != *q)
@@ -170,7 +181,7 @@ lejp_check_path_match(struct lejp_ctx *ctx)
 			continue;
 
 		ctx->path_match = n + 1;
-		ctx->path_match_len = ctx->ppos;
+		ctx->path_match_len = ctx->pst[ctx->pst_sp].ppos;
 		return;
 	}
 
@@ -188,7 +199,7 @@ lejp_get_wildcard(struct lejp_ctx *ctx, int wildcard, char *dest, int len)
 
 	n = ctx->wild[wildcard];
 
-	while (--len && n < ctx->ppos &&
+	while (--len && n < ctx->pst[ctx->pst_sp].ppos &&
 	       (n == ctx->wild[wildcard] || ctx->path[n] != '.'))
 		*dest++ = ctx->path[n++];
 
@@ -222,8 +233,8 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 	static const char esc_tran[] = "\"\\/\b\f\n\r\t";
 	static const char tokens[] = "rue alse ull ";
 
-	if (!ctx->sp && !ctx->ppos)
-		ctx->callback(ctx, LEJPCB_START);
+	if (!ctx->sp && !ctx->pst[ctx->pst_sp].ppos)
+		ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_START);
 
 	while (len--) {
 		c = *json++;
@@ -252,7 +263,7 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				ret = LEJP_REJECT_IDLE_NO_BRACE;
 				goto reject;
 			}
-			if (ctx->callback(ctx, LEJPCB_OBJECT_START)) {
+			if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_OBJECT_START)) {
 				ret = LEJP_REJECT_CALLBACK;
 				goto reject;
 			}
@@ -284,7 +295,7 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				}
 				if (ctx->st[ctx->sp - 1].s != LEJP_MP_DELIM) {
 					ctx->buf[ctx->npos] = '\0';
-					if (ctx->callback(ctx,
+					if (ctx->pst[ctx->pst_sp].callback(ctx,
 						      LEJPCB_VAL_STR_END) < 0) {
 						ret = LEJP_REJECT_CALLBACK;
 						goto reject;
@@ -391,10 +402,10 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				goto reject;
 			}
 			ctx->st[ctx->sp].s = LEJP_MP_VALUE;
-			ctx->path[ctx->ppos] = '\0';
+			ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 
 			lejp_check_path_match(ctx);
-			if (ctx->callback(ctx, LEJPCB_PAIR_NAME)) {
+			if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_PAIR_NAME)) {
 				ret = LEJP_REJECT_CALLBACK;
 				goto reject;
 			}
@@ -415,7 +426,7 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				c = LEJP_MP_STRING;
 				ctx->npos = 0;
 				ctx->buf[0] = '\0';
-				if (ctx->callback(ctx, LEJPCB_VAL_STR_START)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_STR_START)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -426,7 +437,7 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				ctx->st[ctx->sp].s = LEJP_MP_COMMA_OR_END;
 				c = LEJP_MEMBERS;
 				lejp_check_path_match(ctx);
-				if (ctx->callback(ctx, LEJPCB_OBJECT_START)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_OBJECT_START)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -437,10 +448,10 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				/* push */
 				ctx->st[ctx->sp].s = LEJP_MP_ARRAY_END;
 				c = LEJP_MP_VALUE;
-				ctx->path[ctx->ppos++] = '[';
-				ctx->path[ctx->ppos++] = ']';
-				ctx->path[ctx->ppos] = '\0';
-				if (ctx->callback(ctx, LEJPCB_ARRAY_START)) {
+				ctx->path[ctx->pst[ctx->pst_sp].ppos++] = '[';
+				ctx->path[ctx->pst[ctx->pst_sp].ppos++] = ']';
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_ARRAY_START)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -464,12 +475,12 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				}
 				/* drop the path [n] bit */
 				if (ctx->sp) {
-					ctx->ppos = ctx->st[ctx->sp - 1].p;
+					ctx->pst[ctx->pst_sp].ppos = ctx->st[ctx->sp - 1].p;
 					ctx->ipos = ctx->st[ctx->sp - 1].i;
 				}
-				ctx->path[ctx->ppos] = '\0';
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 				if (ctx->path_match &&
-				    ctx->ppos <= ctx->path_match_len)
+				    ctx->pst[ctx->pst_sp].ppos <= ctx->path_match_len)
 					/*
 					 * we shrank the path to be
 					 * smaller than the matching point
@@ -544,12 +555,12 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 
 			ctx->buf[ctx->npos] = '\0';
 			if (ctx->f & LEJP_SEEN_POINT) {
-				if (ctx->callback(ctx, LEJPCB_VAL_NUM_FLOAT)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_NUM_FLOAT)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
 			} else {
-				if (ctx->callback(ctx, LEJPCB_VAL_NUM_INT)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_NUM_INT)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -580,7 +591,7 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 			case 3:
 				ctx->buf[0] = '1';
 				ctx->buf[1] = '\0';
-				if (ctx->callback(ctx, LEJPCB_VAL_TRUE)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_TRUE)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -588,14 +599,14 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 			case 8:
 				ctx->buf[0] = '0';
 				ctx->buf[1] = '\0';
-				if (ctx->callback(ctx, LEJPCB_VAL_FALSE)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_FALSE)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
 				break;
 			case 12:
 				ctx->buf[0] = '\0';
-				if (ctx->callback(ctx, LEJPCB_VAL_NULL)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_NULL)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -605,12 +616,12 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 			break;
 
 		case LEJP_MP_COMMA_OR_END:
-			ctx->path[ctx->ppos] = '\0';
+			ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 			if (c == ',') {
 				/* increment this stack level's index */
 				ctx->st[ctx->sp].s = LEJP_M_P;
 				if (!ctx->sp) {
-					ctx->ppos = 0;
+					ctx->pst[ctx->pst_sp].ppos = 0;
 					/*
 					 * since we came back to root level,
 					 * no path can still match
@@ -618,10 +629,10 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 					ctx->path_match = 0;
 					break;
 				}
-				ctx->ppos = ctx->st[ctx->sp - 1].p;
-				ctx->path[ctx->ppos] = '\0';
+				ctx->pst[ctx->pst_sp].ppos = ctx->st[ctx->sp - 1].p;
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 				if (ctx->path_match &&
-					       ctx->ppos <= ctx->path_match_len)
+						ctx->pst[ctx->pst_sp].ppos <= ctx->path_match_len)
 					/*
 					 * we shrank the path to be
 					 * smaller than the matching point
@@ -649,12 +660,12 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				}
 				/* drop the path [n] bit */
 				if (ctx->sp) {
-					ctx->ppos = ctx->st[ctx->sp - 1].p;
+					ctx->pst[ctx->pst_sp].ppos = ctx->st[ctx->sp - 1].p;
 					ctx->ipos = ctx->st[ctx->sp - 1].i;
 				}
-				ctx->path[ctx->ppos] = '\0';
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 				if (ctx->path_match &&
-					       ctx->ppos <= ctx->path_match_len)
+						ctx->pst[ctx->pst_sp].ppos <= ctx->path_match_len)
 					/*
 					 * we shrank the path to be
 					 * smaller than the matching point
@@ -667,11 +678,11 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 			if (c == '}') {
 				if (!ctx->sp) {
 					lejp_check_path_match(ctx);
-					if (ctx->callback(ctx, LEJPCB_OBJECT_END)) {
+					if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_OBJECT_END)) {
 						ret = LEJP_REJECT_CALLBACK;
 						goto reject;
 					}
-					if (ctx->callback(ctx, LEJPCB_COMPLETE))
+					if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_COMPLETE))
 						goto reject;
 					else
 						/* done, return unused amount */
@@ -680,19 +691,19 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 				/* pop */
 				ctx->sp--;
 				if (ctx->sp) {
-					ctx->ppos = ctx->st[ctx->sp - 1].p;
+					ctx->pst[ctx->pst_sp].ppos = ctx->st[ctx->sp - 1].p;
 					ctx->ipos = ctx->st[ctx->sp - 1].i;
 				}
-				ctx->path[ctx->ppos] = '\0';
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 				if (ctx->path_match &&
-					       ctx->ppos <= ctx->path_match_len)
+						ctx->pst[ctx->pst_sp].ppos <= ctx->path_match_len)
 					/*
 					 * we shrank the path to be
 					 * smaller than the matching point
 					 */
 					ctx->path_match = 0;
 				lejp_check_path_match(ctx);
-				if (ctx->callback(ctx, LEJPCB_OBJECT_END)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_OBJECT_END)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -704,15 +715,15 @@ lejp_parse(struct lejp_ctx *ctx, const unsigned char *json, int len)
 
 		case LEJP_MP_ARRAY_END:
 array_end:
-			ctx->path[ctx->ppos] = '\0';
+			ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 			if (c == ',') {
 				/* increment this stack level's index */
 				if (ctx->ipos)
 					ctx->i[ctx->ipos - 1]++;
 				ctx->st[ctx->sp].s = LEJP_MP_VALUE;
 				if (ctx->sp)
-					ctx->ppos = ctx->st[ctx->sp - 1].p;
-				ctx->path[ctx->ppos] = '\0';
+					ctx->pst[ctx->pst_sp].ppos = ctx->st[ctx->sp - 1].p;
+				ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 				break;
 			}
 			if (c != ']') {
@@ -721,7 +732,7 @@ array_end:
 			}
 
 			ctx->st[ctx->sp].s = LEJP_MP_COMMA_OR_END;
-			ctx->callback(ctx, LEJPCB_ARRAY_END);
+			ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_ARRAY_END);
 			break;
 		}
 
@@ -732,7 +743,7 @@ emit_string_char:
 			/* assemble the string value into chunks */
 			ctx->buf[ctx->npos++] = c;
 			if (ctx->npos == sizeof(ctx->buf) - 1) {
-				if (ctx->callback(ctx, LEJPCB_VAL_STR_CHUNK)) {
+				if (ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_VAL_STR_CHUNK)) {
 					ret = LEJP_REJECT_CALLBACK;
 					goto reject;
 				}
@@ -741,22 +752,22 @@ emit_string_char:
 			continue;
 		}
 		/* name part of name:value pair */
-		ctx->path[ctx->ppos++] = c;
+		ctx->path[ctx->pst[ctx->pst_sp].ppos++] = c;
 		continue;
 
 add_stack_level:
 		/* push on to the object stack */
-		if (ctx->ppos && ctx->st[ctx->sp].s != LEJP_MP_COMMA_OR_END &&
+		if (ctx->pst[ctx->pst_sp].ppos && ctx->st[ctx->sp].s != LEJP_MP_COMMA_OR_END &&
 				ctx->st[ctx->sp].s != LEJP_MP_ARRAY_END)
-			ctx->path[ctx->ppos++] = '.';
+			ctx->path[ctx->pst[ctx->pst_sp].ppos++] = '.';
 
-		ctx->st[ctx->sp].p = ctx->ppos;
+		ctx->st[ctx->sp].p = ctx->pst[ctx->pst_sp].ppos;
 		ctx->st[ctx->sp].i = ctx->ipos;
 		if (++ctx->sp == LWS_ARRAY_SIZE(ctx->st)) {
 			ret = LEJP_REJECT_STACK_OVERFLOW;
 			goto reject;
 		}
-		ctx->path[ctx->ppos] = '\0';
+		ctx->path[ctx->pst[ctx->pst_sp].ppos] = '\0';
 		ctx->st[ctx->sp].s = c;
 		ctx->st[ctx->sp].b = 0;
 		continue;
@@ -777,10 +788,55 @@ redo_character:
 	return LEJP_CONTINUE;
 
 reject:
-	ctx->callback(ctx, LEJPCB_FAILED);
+	ctx->pst[ctx->pst_sp].callback(ctx, LEJPCB_FAILED);
 	return ret;
 }
 
+int
+lejp_parser_push(struct lejp_ctx *ctx, void *user, const char * const *paths,
+		 unsigned char paths_count, lejp_callback lejp_cb)
+{
+	struct _lejp_parsing_stack *p;
+
+	if (ctx->pst_sp + 1 == LEJP_MAX_PARSING_STACK_DEPTH)
+		return -1;
+
+	lejp_check_path_match(ctx);
+
+	ctx->pst[ctx->pst_sp].path_match = ctx->path_match;
+	ctx->pst_sp++;
+
+	p = &ctx->pst[ctx->pst_sp];
+	p->user = user;
+	p->callback = lejp_cb;
+	p->paths = paths;
+	p->count_paths = paths_count;
+	p->ppos = 0;
+
+	ctx->path_match = 0;
+	lejp_check_path_match(ctx);
+
+	lwsl_debug("%s: pushed parser stack to %d (path %s)\n", __func__,
+		   ctx->pst_sp, ctx->path);
+
+	return 0;
+}
+
+int
+lejp_parser_pop(struct lejp_ctx *ctx)
+{
+	if (!ctx->pst_sp)
+		return -1;
+
+	ctx->pst_sp--;
+	lwsl_debug("%s: popped parser stack to %d\n", __func__, ctx->pst_sp);
+
+	ctx->path_match = 0; /* force it to check */
+	lejp_check_path_match(ctx);
+
+	return 0;
+}
+
 const char *
 lejp_error_to_string(int e)
 {
diff --git a/lib/misc/lws-struct-lejp.c b/lib/misc/lws-struct-lejp.c
new file mode 100644
index 0000000000000000000000000000000000000000..af637cc3b37d4a77f77bca227ca3cbdcf8bca601
--- /dev/null
+++ b/lib/misc/lws-struct-lejp.c
@@ -0,0 +1,762 @@
+/*
+ * libwebsockets - lws_struct JSON serialization helpers
+ *
+ * Copyright (C) 2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+
+#include <libwebsockets.h>
+#include <core/private.h>
+
+#include <assert.h>
+
+signed char
+lws_struct_schema_only_lejp_cb(struct lejp_ctx *ctx, char reason)
+{
+	lws_struct_args_t *a = (lws_struct_args_t *)ctx->user;
+	const lws_struct_map_t *map = a->map_st[ctx->pst_sp];
+	int n = a->map_entries_st[ctx->pst_sp];
+	lejp_callback cb = map->lejp_cb;
+
+	if (reason != LEJPCB_VAL_STR_END || ctx->path_match != 1)
+		return 0;
+
+	while (n--) {
+		if (strcmp(ctx->buf, map->colname)) {
+			map++;
+			continue;
+		}
+
+		a->dest = lwsac_use_zero(&a->ac, map->aux, a->ac_block_size);
+		if (!a->dest) {
+			lwsl_err("%s: OOT\n", __func__);
+
+			return 1;
+		}
+		a->dest_len = map->aux;
+
+		if (!cb)
+			cb = lws_struct_default_lejp_cb;
+
+		lejp_parser_push(ctx, a->dest, &map->child_map[0].colname,
+				 (uint8_t)map->child_map_size, cb);
+		a->map_st[ctx->pst_sp] = map->child_map;
+		a->map_entries_st[ctx->pst_sp] = map->child_map_size;
+
+		return 0;
+	}
+
+	lwsl_notice("%s: unknown schema %s\n", __func__, ctx->buf);
+
+	return 1;
+}
+
+static int
+lws_struct_lejp_push(struct lejp_ctx *ctx, lws_struct_args_t *args,
+		     const lws_struct_map_t *map, uint8_t *ch)
+{
+	lejp_callback cb = map->lejp_cb;
+
+	if (!cb)
+		cb = lws_struct_default_lejp_cb;
+
+	lejp_parser_push(ctx, ch, (const char * const*)map->child_map,
+			 (uint8_t)map->child_map_size, cb);
+
+	args->map_st[ctx->pst_sp] = map->child_map;
+	args->map_entries_st[ctx->pst_sp] = map->child_map_size;
+
+	return 0;
+}
+
+signed char
+lws_struct_default_lejp_cb(struct lejp_ctx *ctx, char reason)
+{
+	lws_struct_args_t *args = (lws_struct_args_t *)ctx->user;
+	const lws_struct_map_t *map, *pmap = NULL;
+	uint8_t *ch;
+	char *u;
+	int n;
+
+	if (reason == LEJPCB_ARRAY_END) {
+		lejp_parser_pop(ctx);
+
+		return 0;
+	}
+
+	if (reason == LEJPCB_ARRAY_START) {
+		map = &args->map_st[ctx->pst_sp][ctx->path_match - 1];
+		n = args->map_entries_st[ctx->pst_sp];
+
+		if (map->type == LSMT_LIST)
+			lws_struct_lejp_push(ctx, args, map, NULL);
+
+		return 0;
+	}
+
+	if (ctx->pst_sp)
+		pmap = &args->map_st[ctx->pst_sp - 1]
+	                 [ctx->pst[ctx->pst_sp - 1].path_match - 1];
+	map = &args->map_st[ctx->pst_sp][ctx->path_match - 1];
+	n = args->map_entries_st[ctx->pst_sp];
+
+	if (reason == LEJPCB_OBJECT_START) {
+
+		if (map->type != LSMT_CHILD_PTR) {
+			ctx->pst[ctx->pst_sp].user = NULL;
+
+			return 0;
+		}
+		pmap = map;
+
+		lws_struct_lejp_push(ctx, args, map, NULL);
+		map = &args->map_st[ctx->pst_sp][ctx->path_match - 1];
+		n = args->map_entries_st[ctx->pst_sp];
+	}
+
+	if (reason == LEJPCB_OBJECT_END && pmap && pmap->type == LSMT_CHILD_PTR)
+		lejp_parser_pop(ctx);
+
+	if (map->type == LSMT_SCHEMA) {
+
+		while (n--) {
+			if (strcmp(map->colname, ctx->buf)) {
+				map++;
+				continue;
+			}
+
+			/* instantiate the correct toplevel object */
+
+			ch = lwsac_use_zero(&args->ac, map->aux,
+					    args->ac_block_size);
+			if (!ch) {
+				lwsl_err("OOM\n");
+
+				return 1;
+			}
+
+			lws_struct_lejp_push(ctx, args, map, ch);
+
+			return 0;
+		}
+		lwsl_notice("%s: unknown schema\n", __func__);
+
+		goto cleanup;
+	}
+
+	if (!ctx->pst[ctx->pst_sp].user) {
+		struct lws_dll2_owner *owner;
+		struct lws_dll2 *list;
+
+		/* create list item object if none already */
+
+		if (!ctx->path_match || !pmap)
+			return 0;
+
+		map = &args->map_st[ctx->pst_sp - 1][ctx->path_match - 1];
+		n = args->map_entries_st[ctx->pst_sp - 1];
+
+		if (pmap->type != LSMT_LIST && pmap->type != LSMT_CHILD_PTR)
+			return 1;
+
+		/* we need to create a child or array item object */
+
+		owner = (struct lws_dll2_owner *)
+			(((char *)ctx->pst[ctx->pst_sp - 1].user) + pmap->ofs);
+
+		assert(pmap->aux);
+
+		/* instantiate one of the child objects */
+
+		ctx->pst[ctx->pst_sp].user = lwsac_use_zero(&args->ac,
+						pmap->aux, args->ac_block_size);
+		if (!ctx->pst[ctx->pst_sp].user) {
+			lwsl_err("OOM\n");
+
+			return 1;
+		}
+		lwsl_notice("%s: created child object size %d\n", __func__,
+				(int)pmap->aux);
+
+		if (pmap->type == LSMT_LIST) {
+			list = (struct lws_dll2 *)((char *)ctx->pst[ctx->pst_sp].user +
+				map->ofs_clist);
+
+			lws_dll2_add_tail(list, owner);
+		}
+	}
+
+	if (!ctx->path_match)
+		return 0;
+
+	if (reason == LEJPCB_VAL_STR_CHUNK) {
+		lejp_collation_t *coll;
+
+		/* don't cache stuff we are going to ignore */
+
+		if (map->type == LSMT_STRING_CHAR_ARRAY &&
+		    args->chunks_length >= map->aux)
+			return 0;
+
+		coll = lwsac_use_zero(&args->ac_chunks, sizeof(*coll),
+				      sizeof(*coll));
+		if (!coll) {
+			lwsl_err("%s: OOT\n", __func__);
+
+			return 1;
+		}
+		coll->chunks.prev = NULL;
+		coll->chunks.next = NULL;
+		coll->chunks.owner = NULL;
+
+		coll->len = ctx->npos;
+		lws_dll2_add_tail(&coll->chunks, &args->chunks_owner);
+
+		memcpy(coll->buf, ctx->buf, ctx->npos);
+
+		args->chunks_length += ctx->npos;
+
+		return 0;
+	}
+
+	if (reason != LEJPCB_VAL_STR_END && reason != LEJPCB_VAL_NUM_INT &&
+	    reason != LEJPCB_VAL_TRUE && reason != LEJPCB_VAL_FALSE)
+		return 0;
+
+	/* this is the end of the string */
+
+	if (ctx->pst[ctx->pst_sp].user && pmap && pmap->type == LSMT_CHILD_PTR) {
+		void **pp = (void **)
+			(((char *)ctx->pst[ctx->pst_sp - 1].user) + pmap->ofs);
+
+		*pp = ctx->pst[ctx->pst_sp].user;
+	}
+
+	u = (char *)ctx->pst[ctx->pst_sp].user;
+	if (!u)
+		u = (char *)ctx->pst[ctx->pst_sp - 1].user;
+
+	{
+		char **pp, *s;
+		size_t lim, b;
+		long long li;
+
+		switch (map->type) {
+		case LSMT_SIGNED:
+			if (map->aux == sizeof(signed char)) {
+				signed char *pc;
+				pc = (signed char *)(u + map->ofs);
+				*pc = atoi(ctx->buf);
+				break;
+			}
+			if (map->aux == sizeof(int)) {
+				int *pi;
+				pi = (int *)(u + map->ofs);
+				*pi = atoi(ctx->buf);
+				break;
+			}
+			if (map->aux == sizeof(long)) {
+				long *pl;
+				pl = (long *)(u + map->ofs);
+				*pl = atol(ctx->buf);
+			} else {
+				long long *pll;
+				pll = (long long *)(u + map->ofs);
+				*pll = atoll(ctx->buf);
+			}
+			break;
+
+		case LSMT_UNSIGNED:
+			if (map->aux == sizeof(unsigned char)) {
+				unsigned char *pc;
+				pc = (unsigned char *)(u + map->ofs);
+				*pc = atoi(ctx->buf);
+				break;
+			}
+			if (map->aux == sizeof(unsigned int)) {
+				unsigned int *pi;
+				pi = (unsigned int *)(u + map->ofs);
+				*pi = atoi(ctx->buf);
+				break;
+			}
+			if (map->aux == sizeof(unsigned long)) {
+				unsigned long *pl;
+				pl = (unsigned long *)(u + map->ofs);
+				*pl = atol(ctx->buf);
+			} else {
+				unsigned long long *pll;
+				pll = (unsigned long long *)(u + map->ofs);
+				*pll = atoll(ctx->buf);
+			}
+			break;
+
+		case LSMT_BOOLEAN:
+			li = reason == LEJPCB_VAL_TRUE;
+			if (map->aux == sizeof(char)) {
+				char *pc;
+				pc = (char *)(u + map->ofs);
+				*pc = (char)li;
+				break;
+			}
+			if (map->aux == sizeof(int)) {
+				int *pi;
+				pi = (int *)(u + map->ofs);
+				*pi = (int)li;
+			} else {
+				uint64_t *p64;
+				p64 = (uint64_t *)(u + map->ofs);
+				*p64 = li;
+			}
+			break;
+
+		case LSMT_STRING_CHAR_ARRAY:
+			s = (char *)(u + map->ofs);
+			lim = map->aux - 1;
+			goto chunk_copy;
+
+		case LSMT_STRING_PTR:
+			pp = (char **)(u + map->ofs);
+			lim = args->chunks_length + ctx->npos;
+			s = lwsac_use(&args->ac, lim + 1, args->ac_block_size);
+			if (!s)
+				goto cleanup;
+			*pp = s;
+
+chunk_copy:
+			s[lim] = '\0';
+			/* copy up to lim from the string chunk ac first */
+			lws_start_foreach_dll_safe(struct lws_dll2 *, p, p1,
+						args->chunks_owner.head) {
+				lejp_collation_t *coll = (lejp_collation_t *)p;
+
+				if (lim) {
+					b = coll->len;
+					if (b > lim)
+						b = lim;
+					memcpy(s, coll->buf, b);
+					s += b;
+					lim -= b;
+				}
+			} lws_end_foreach_dll_safe(p, p1);
+
+			lwsac_free(&args->ac_chunks);
+			args->chunks_owner.count = 0;
+			args->chunks_owner.head = NULL;
+			args->chunks_owner.tail = NULL;
+
+			if (lim) {
+				b = ctx->npos;
+				if (b > lim)
+					b = lim;
+				memcpy(s, ctx->buf, b);
+			}
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (args->cb)
+		args->cb(args->dest, args->cb_arg);
+
+	return 0;
+
+cleanup:
+	lwsl_notice("%s: cleanup\n", __func__);
+	lwsac_free(&args->ac_chunks);
+	args->chunks_owner.count = 0;
+	args->chunks_owner.head = NULL;
+	args->chunks_owner.tail = NULL;
+
+	return 1;
+}
+
+static const char * schema[] = { "schema" };
+
+int
+lws_struct_json_init_parse(struct lejp_ctx *ctx, lejp_callback cb, void *user)
+{
+	if (!cb)
+		cb = lws_struct_schema_only_lejp_cb;
+	lejp_construct(ctx, cb, user, schema, 1);
+
+	ctx->path_stride = sizeof(lws_struct_map_t);
+
+	return 0;
+}
+
+lws_struct_serialize_t *
+lws_struct_json_serialize_create(const lws_struct_map_t *map,
+				 size_t map_entries, int flags,
+				 void *ptoplevel)
+{
+	lws_struct_serialize_t *js = lws_zalloc(sizeof(*js), __func__);
+	lws_struct_serialize_st_t *j;
+
+	if (!js)
+		return NULL;
+
+	js->flags = flags;
+
+	j = &js->st[0];
+	j->map = map;
+	j->map_entries = map_entries;
+	j->obj = ptoplevel;
+	j->idt = 0;
+
+	return js;
+}
+
+void
+lws_struct_json_serialize_destroy(lws_struct_serialize_t **pjs)
+{
+	if (!*pjs)
+		return;
+
+	lws_free(*pjs);
+
+	*pjs = NULL;
+}
+
+static void
+lws_struct_pretty(lws_struct_serialize_t *js, uint8_t **pbuf, size_t *plen)
+{
+	if (js->flags & LSSERJ_FLAG_PRETTY) {
+		int n;
+
+		*(*pbuf)++ = '\n';
+		(*plen)--;
+		for (n = 0; n < js->st[js->sp].idt; n++) {
+			*(*pbuf)++ = ' ';
+			(*plen)--;
+		}
+	}
+}
+
+lws_struct_json_serialize_result_t
+lws_struct_json_serialize(lws_struct_serialize_t *js, uint8_t *buf,
+			  size_t len, size_t *written)
+{
+	lws_struct_serialize_st_t *j;
+	const lws_struct_map_t *map;
+	size_t budget = 0, olen = len;
+	struct lws_dll2_owner *o;
+	unsigned long long uli;
+	const char *q;
+	const void *p;
+	char dbuf[72];
+	long long li;
+	int n;
+
+	*written = 0;
+	*buf = '\0';
+
+	while (len > sizeof(dbuf) + 20) {
+		j = &js->st[js->sp];
+		map = &j->map[j->map_entry];
+		q = j->obj + map->ofs;
+
+		/* early check if the entry should be elided */
+
+		switch (map->type) {
+		case LSMT_STRING_PTR:
+		case LSMT_CHILD_PTR:
+			q = (char *)*(char **)q;
+			if (!q)
+				goto up;
+			break;
+
+		case LSMT_LIST:
+			o = (struct lws_dll2_owner *)q;
+			p = j->dllpos = lws_dll2_get_head(o);
+			if (!p)
+				goto up;
+			break;
+
+		default:
+			break;
+		}
+
+		if (j->subsequent) {
+			*buf++ = ',';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+		}
+		j->subsequent = 1;
+
+		if (map->type != LSMT_SCHEMA && !js->offset) {
+			n = lws_snprintf((char *)buf, len, "\"%s\":",
+					    map->colname);
+			buf += n;
+			len -= n;
+			if (js->flags & LSSERJ_FLAG_PRETTY) {
+				*buf++ = ' ';
+				len--;
+			}
+		}
+
+		switch (map->type) {
+		case LSMT_BOOLEAN:
+		case LSMT_UNSIGNED:
+			if (map->aux == sizeof(char)) {
+				uli = *(unsigned char *)q;
+			} else {
+				if (map->aux == sizeof(int)) {
+					uli = *(unsigned int *)q;
+				} else {
+					if (map->aux == sizeof(long))
+						uli = *(unsigned long *)q;
+					else
+						uli = *(unsigned long long *)q;
+				}
+			}
+			q = dbuf;
+
+			if (map->type == LSMT_BOOLEAN) {
+				budget = lws_snprintf(dbuf, sizeof(dbuf),
+						"%s", uli ? "true" : "false");
+			} else
+				budget = lws_snprintf(dbuf, sizeof(dbuf),
+						      "%llu", uli);
+			break;
+
+		case LSMT_SIGNED:
+			if (map->aux == sizeof(signed char)) {
+				li = (long long)*(signed char *)q;
+			} else {
+				if (map->aux == sizeof(int)) {
+					li = (long long)*(int *)q;
+				} else {
+					if (map->aux == sizeof(long))
+						li = (long long)*(long *)q;
+					else
+						li = *(long long *)q;
+				}
+			}
+			q = dbuf;
+			budget = lws_snprintf(dbuf, sizeof(dbuf), "%lld", li);
+			break;
+
+		case LSMT_STRING_CHAR_ARRAY:
+			budget = strlen(q);
+			if (!js->offset) {
+				*buf++ = '\"';
+				len--;
+			}
+			break;
+
+		case LSMT_STRING_PTR:
+			budget = strlen(q);
+			if (!js->offset) {
+				*buf++ = '\"';
+				len--;
+			}
+			break;
+		case LSMT_LIST:
+			*buf++ = '[';
+			len--;
+			if (js->sp + 1 == LEJP_MAX_PARSING_STACK_DEPTH)
+				return LSJS_RESULT_ERROR;
+
+			/* add a stack level to handle parsing array members */
+
+			o = (struct lws_dll2_owner *)q;
+			p = j->dllpos = lws_dll2_get_head(o);
+
+			if (!j->dllpos) {
+				*buf++ = ']';
+				len--;
+				goto up;
+			}
+
+			n = j->idt;
+			j = &js->st[++js->sp];
+			j->idt = n + 2;
+			j->map = map->child_map;
+			j->map_entries = map->child_map_size;
+			j->size = map->aux;
+			j->subsequent = 0;
+			j->map_entry = 0;
+			lws_struct_pretty(js, &buf, &len);
+			*buf++ = '{';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+			if (p)
+				j->obj = ((char *)p) - j->map->ofs_clist;
+			else
+				j->obj = NULL;
+			continue;
+
+		case LSMT_CHILD_PTR:
+
+			if (js->sp + 1 == LEJP_MAX_PARSING_STACK_DEPTH)
+				return LSJS_RESULT_ERROR;
+
+			/* add a stack level tto handle parsing child members */
+
+			n = j->idt;
+			j = &js->st[++js->sp];
+			j->idt = n + 2;
+			j->map = map->child_map;
+			j->map_entries = map->child_map_size;
+			j->size = map->aux;
+			j->subsequent = 0;
+			j->map_entry = 0;
+			*buf++ = '{';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+			j->obj = q;
+			continue;
+
+		case LSMT_SCHEMA:
+			q = dbuf;
+			*buf++ = '{';
+			len--;
+			j = &js->st[++js->sp];
+			lws_struct_pretty(js, &buf, &len);
+			budget = lws_snprintf(dbuf, 15, "\"schema\":");
+			if (js->flags & LSSERJ_FLAG_PRETTY)
+				dbuf[budget++] = ' ';
+
+			budget += lws_snprintf(dbuf + budget,
+					       sizeof(dbuf) - budget,
+					      "\"%s\"", map->colname);
+
+
+			if (js->sp != 1)
+				return LSJS_RESULT_ERROR;
+			j->map = map->child_map;
+			j->map_entries = map->child_map_size;
+			j->size = map->aux;
+			j->subsequent = 0;
+			j->map_entry = 0;
+			j->obj = js->st[js->sp - 1].obj;
+			j->dllpos = NULL;
+			/* we're actually at the same level */
+			j->subsequent = 1;
+			j->idt = 1;
+			break;
+		}
+
+		q += js->offset;
+		budget -= js->remaining;
+
+		if (budget > len) {
+			js->remaining = budget - len;
+			js->offset = len;
+			budget = len;
+		} else {
+			js->remaining = 0;
+			js->offset = 0;
+		}
+
+		memcpy(buf, q, budget);
+		buf += budget;
+		*buf = '\0';
+		len -= budget;
+
+		switch (map->type) {
+		case LSMT_STRING_CHAR_ARRAY:
+		case LSMT_STRING_PTR:
+			*buf++ = '\"';
+			len--;
+			break;
+		case LSMT_SCHEMA:
+			continue;
+		default:
+			break;
+		}
+
+		if (js->remaining)
+			continue;
+up:
+		if (++j->map_entry < j->map_entries)
+			continue;
+
+		if (!js->sp)
+			continue;
+		js->sp--;
+		if (!js->sp) {
+			lws_struct_pretty(js, &buf, &len);
+			*buf++ = '}';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+			break;
+		}
+		js->offset = 0;
+		j = &js->st[js->sp];
+		map = &j->map[j->map_entry];
+
+		if (map->type == LSMT_CHILD_PTR) {
+			lws_struct_pretty(js, &buf, &len);
+			*buf++ = '}';
+			len--;
+
+			/* we have done the singular child pointer */
+
+			js->offset = 0;
+			goto up;
+		}
+
+		if (map->type != LSMT_LIST)
+			continue;
+		/*
+		 * we are coming back up to an array map, it means we should
+		 * advance to the next array member if there is one
+		 */
+
+		lws_struct_pretty(js, &buf, &len);
+		*buf++ = '}';
+		len--;
+
+		p = j->dllpos = j->dllpos->next;
+		if (j->dllpos) {
+			/*
+			 * there was another item in the array to do... let's
+			 * move on to that nd do it
+			 */
+			*buf++ = ',';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+			js->offset = 0;
+			j = &js->st[++js->sp];
+			j->map_entry = 0;
+			map = &j->map[j->map_entry];
+
+			*buf++ = '{';
+			len--;
+			lws_struct_pretty(js, &buf, &len);
+
+			j->subsequent = 0;
+			j->obj = ((char *)p) - j->map->ofs_clist;
+			continue;
+		}
+
+		/* there are no further items in the array */
+
+		js->offset = 0;
+		lws_struct_pretty(js, &buf, &len);
+		*buf++ = ']';
+		len--;
+		goto up;
+	}
+
+	*written = olen - len;
+	*buf = '\0'; /* convenience, a NUL after the official end */
+
+	return LSJS_RESULT_FINISH;
+}
diff --git a/lib/misc/lws-struct-sqlite.c b/lib/misc/lws-struct-sqlite.c
new file mode 100644
index 0000000000000000000000000000000000000000..2ed2a4ef1bf1cf5a85b2371acd0ef9588e5d3734
--- /dev/null
+++ b/lib/misc/lws-struct-sqlite.c
@@ -0,0 +1,275 @@
+/*
+ * libwebsockets - lws_struct JSON serialization helpers
+ *
+ * Copyright (C) 2019 Andy Green <andy@warmcat.com>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation:
+ *  version 2.1 of the License.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301  USA
+ */
+
+#include <libwebsockets.h>
+#include <core/private.h>
+
+#include <sqlite3.h>
+
+/*
+ * we get one of these per matching result from the query
+ */
+
+static int
+lws_struct_sq3_deser_cb(void *priv, int cols, char **cv, char **cn)
+{
+	lws_struct_args_t *a = (lws_struct_args_t *)priv;
+	const lws_struct_map_t *map = a->map_st[0];
+	int n, mems = a->map_entries_st[0];
+	lws_dll2_owner_t *o = (lws_dll2_owner_t *)a->cb_arg;
+	char *u = lwsac_use_zero(&a->ac, a->dest_len, a->ac_block_size);
+	long long li;
+	size_t lim;
+	char **pp;
+	char *s;
+
+	if (!u) {
+		lwsl_err("OOM\n");
+
+		return 1;
+	}
+
+	lws_dll2_add_tail((lws_dll2_t *)((char *)u + a->toplevel_dll2_ofs), o);
+
+	while (mems--) {
+		for (n = 0; n < cols; n++) {
+			if (!cv[n] || strcmp(cn[n], map->colname))
+				continue;
+
+			switch (map->type) {
+			case LSMT_SIGNED:
+				if (map->aux == sizeof(signed char)) {
+					signed char *pc;
+					pc = (signed char *)(u + map->ofs);
+					*pc = atoi(cv[n]);
+					break;
+				}
+				if (map->aux == sizeof(int)) {
+					int *pi;
+					pi = (int *)(u + map->ofs);
+					*pi = atoi(cv[n]);
+					break;
+				}
+				if (map->aux == sizeof(long)) {
+					long *pl;
+					pl = (long *)(u + map->ofs);
+					*pl = atol(cv[n]);
+					break;
+				}
+				{
+					long long *pll;
+					pll = (long long *)(u + map->ofs);
+					*pll = atoll(cv[n]);
+				}
+				break;
+
+			case LSMT_UNSIGNED:
+				if (map->aux == sizeof(unsigned char)) {
+					unsigned char *pc;
+					pc = (unsigned char *)(u + map->ofs);
+					*pc = atoi(cv[n]);
+					break;
+				}
+				if (map->aux == sizeof(unsigned int)) {
+					unsigned int *pi;
+					pi = (unsigned int *)(u + map->ofs);
+					*pi = atoi(cv[n]);
+					break;
+				}
+				if (map->aux == sizeof(unsigned long)) {
+					unsigned long *pl;
+					pl = (unsigned long *)(u + map->ofs);
+					*pl = atol(cv[n]);
+					break;
+				}
+				{
+					unsigned long long *pll;
+					pll = (unsigned long long *)(u + map->ofs);
+					*pll = atoll(cv[n]);
+				}
+				break;
+
+			case LSMT_BOOLEAN:
+				li = 0;
+				if (!strcmp(cv[n], "true") ||
+				    !strcmp(cv[n], "TRUE") || cv[n][0] == '1')
+					li = 1;
+				if (map->aux == sizeof(char)) {
+					char *pc;
+					pc = (char *)(u + map->ofs);
+					*pc = (char)li;
+					break;
+				}
+				if (map->aux == sizeof(int)) {
+					int *pi;
+					pi = (int *)(u + map->ofs);
+					*pi = (int)li;
+				} else {
+					uint64_t *p64;
+					p64 = (uint64_t *)(u + map->ofs);
+					*p64 = li;
+				}
+				break;
+
+			case LSMT_STRING_CHAR_ARRAY:
+				s = (char *)(u + map->ofs);
+				lim = map->aux - 1;
+				lws_strncpy(s, cv[n], lim);
+				break;
+
+			case LSMT_STRING_PTR:
+				pp = (char **)(u + map->ofs);
+				lim = strlen(cv[n]);
+				s = lwsac_use(&a->ac, lim + 1, a->ac_block_size);
+				if (!s)
+					return 1;
+				*pp = s;
+				memcpy(s, cv[n], lim);
+				s[lim] = '\0';
+				break;
+			default:
+				break;
+			}
+		}
+		map++;
+	}
+
+	return 0;
+}
+
+/*
+ * Call this with an LSM_SCHEMA map, its colname is the table name and its
+ * type information describes the toplevel type.  Schema is dereferenced and
+ * put in args before the actual sq3 query, which is given the child map.
+ */
+
+int
+lws_struct_sq3_deserialize(sqlite3 *pdb, const lws_struct_map_t *schema,
+			   lws_dll2_owner_t *o, struct lwsac **ac,
+			   uint64_t start, int limit)
+{
+	char s[150], where[32];
+	lws_struct_args_t a;
+
+	memset(&a, 0, sizeof(a));
+	a.cb_arg = o; /* lws_dll2_owner tracking query result objects */
+	a.map_st[0]  = schema->child_map;
+	a.map_entries_st[0] = schema->child_map_size;
+	a.dest_len = schema->aux; /* size of toplevel object to allocate */
+	a.toplevel_dll2_ofs = schema->ofs;
+
+	lws_dll2_owner_clear(o);
+
+	where[0] = '\0';
+	if (start)
+		lws_snprintf(where, sizeof(where), " where when < %llu ",
+				(unsigned long long)start);
+
+	lws_snprintf(s, sizeof(s) - 1, "select * "
+		     "from %s %s order by created desc limit %d;",
+		     schema->colname, where, limit);
+
+	if (sqlite3_exec(pdb, s, lws_struct_sq3_deser_cb, &a, NULL) != SQLITE_OK) {
+		lwsl_err("%s: fail\n", sqlite3_errmsg(pdb));
+		lwsac_free(&a.ac);
+		return -1;
+	}
+
+	*ac = a.ac;
+
+	return 0;
+}
+
+int
+lws_struct_sq3_create_table(sqlite3 *pdb, const lws_struct_map_t *schema)
+{
+	const lws_struct_map_t *map = schema->child_map;
+	int map_size = schema->child_map_size, subsequent = 0;
+	char s[2048], *p = s, *end = &s[sizeof(s) - 1], *pri = "primary key";
+
+	p += lws_snprintf(p, end - p, "create table if not exists %s (",
+			  schema->colname);
+
+	while (map_size--) {
+		if (map->type > LSMT_STRING_PTR) {
+			map++;
+			continue;
+		}
+		if (subsequent && (end - p) > 3)
+			*p++ = ',';
+		subsequent = 1;
+		if (map->type < LSMT_STRING_CHAR_ARRAY)
+			p += lws_snprintf(p, end - p, "%s integer %s",
+					  map->colname, pri);
+		else
+			p += lws_snprintf(p, end - p, "%s varchar %s",
+					  map->colname, pri);
+		pri = "";
+		map++;
+	}
+
+	p += lws_snprintf(p, end - p, ");");
+
+	if (sqlite3_exec(pdb, s, NULL, NULL, NULL) != SQLITE_OK) {
+		lwsl_err("%s: %s: fail\n", __func__, sqlite3_errmsg(pdb));
+
+		return -1;
+	}
+
+	return 0;
+}
+
+int
+lws_struct_sq3_open(struct lws_context *context, const char *sqlite3_path,
+		    sqlite3 **pdb)
+{
+	int uid = 0, gid = 0;
+
+	if (sqlite3_open_v2(sqlite3_path, pdb,
+			    SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
+			    NULL) != SQLITE_OK) {
+		lwsl_err("%s: Unable to open db %s: %s\n",
+			 __func__, sqlite3_path, sqlite3_errmsg(*pdb));
+
+		return 1;
+	}
+
+	lws_get_effective_uid_gid(context, &uid, &gid);
+	if (uid)
+		chown(sqlite3_path, uid, gid);
+	chmod(sqlite3_path, 0600);
+
+	sqlite3_extended_result_codes(*pdb, 1);
+
+	return 0;
+}
+
+int
+lws_struct_sq3_close(sqlite3 **pdb)
+{
+	if (!*pdb)
+		return 0;
+
+	sqlite3_close(*pdb);
+	*pdb = NULL;
+
+	return 0;
+}
diff --git a/lib/misc/lwsac/lwsac.c b/lib/misc/lwsac/lwsac.c
index a401bbc7f6c218fcbf3460625a4c2fd209878e86..6471c1652ee9d4f1059b92471ab602f5d3134c55 100644
--- a/lib/misc/lwsac/lwsac.c
+++ b/lib/misc/lwsac/lwsac.c
@@ -158,6 +158,7 @@ lwsac_free(struct lwsac **head)
 {
 	struct lwsac *it = *head;
 
+	*head = NULL;
 	lwsl_debug("%s: head %p\n", __func__, *head);
 
 	while (it) {
@@ -166,14 +167,15 @@ lwsac_free(struct lwsac **head)
 		free(it);
 		it = tmp;
 	}
-
-	*head = NULL;
 }
 
 void
 lwsac_info(struct lwsac *head)
 {
-	lwsl_debug("%s: lac %p: %dKiB in %d blocks\n", __func__, head,
+	if (!head)
+		lwsl_debug("%s: empty\n", __func__);
+	else
+		lwsl_debug("%s: lac %p: %dKiB in %d blocks\n", __func__, head,
 		   (int)(head->total_alloc_size >> 10), head->total_blocks);
 }
 
diff --git a/minimal-examples/api-tests/README.md b/minimal-examples/api-tests/README.md
index ff8d48ed370965d4fc140e6f42d65e8b1316d06f..8a1477d3b52ac4aa32316da8c3d226c7f7a1680d 100644
--- a/minimal-examples/api-tests/README.md
+++ b/minimal-examples/api-tests/README.md
@@ -3,6 +3,7 @@ These are buildable test apps that run in CI to confirm correct api operation.
 |name|tests|
 ---|---
 api-test-lwsac|LWS Allocated Chunks api
+api-test-lws_struct-json|Selftests for lws_struct JSON serialization and deserialization
 api-test-lws_tokenize|Generic secure string tokenizer api
 api-test-fts|LWS Full-text Search api
 api-test-gencrypto|LWS Generic Crypto apis
diff --git a/minimal-examples/api-tests/api-test-lws_struct-json/CMakeLists.txt b/minimal-examples/api-tests/api-test-lws_struct-json/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a0900f866b2530e039d41b28b48c0dabb2543d57
--- /dev/null
+++ b/minimal-examples/api-tests/api-test-lws_struct-json/CMakeLists.txt
@@ -0,0 +1,73 @@
+cmake_minimum_required(VERSION 2.8)
+include(CheckCSourceCompiles)
+
+set(SAMP lws-api-test-lws_struct-json)
+set(SRCS main.c)
+
+# If we are being built as part of lws, confirm current build config supports
+# reqconfig, else skip building ourselves.
+#
+# If we are being built externally, confirm installed lws was configured to
+# support reqconfig, else error out with a helpful message about the problem.
+#
+MACRO(require_lws_config reqconfig _val result)
+
+	if (DEFINED ${reqconfig})
+	if (${reqconfig})
+		set (rq 1)
+	else()
+		set (rq 0)
+	endif()
+	else()
+		set(rq 0)
+	endif()
+
+	if (${_val} EQUAL ${rq})
+		set(SAME 1)
+	else()
+		set(SAME 0)
+	endif()
+
+	if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME})
+		if (${_val})
+			message("${SAMP}: skipping as lws being built without ${reqconfig}")
+		else()
+			message("${SAMP}: skipping as lws built with ${reqconfig}")
+		endif()
+		set(${result} 0)
+	else()
+		if (LWS_WITH_MINIMAL_EXAMPLES)
+			set(MET ${SAME})
+		else()
+			CHECK_C_SOURCE_COMPILES("#include <libwebsockets.h>\nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig})
+			if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig})
+				set(HAS_${reqconfig} 0)
+			else()
+				set(HAS_${reqconfig} 1)
+			endif()
+			if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val}))
+				set(MET 1)
+			else()
+				set(MET 0)
+			endif()
+		endif()
+		if (NOT MET)
+			if (${_val})
+				message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}")
+			else()
+				message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project")
+			endif()
+		endif()
+	endif()
+ENDMACRO()
+
+
+
+	add_executable(${SAMP} ${SRCS})
+
+	if (websockets_shared)
+		target_link_libraries(${SAMP} websockets_shared)
+		add_dependencies(${SAMP} websockets_shared)
+	else()
+		target_link_libraries(${SAMP} websockets)
+	endif()
diff --git a/minimal-examples/api-tests/api-test-lws_struct-json/README.md b/minimal-examples/api-tests/api-test-lws_struct-json/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ebe930d24f2e11b7a815c875552b82b465e731ad
--- /dev/null
+++ b/minimal-examples/api-tests/api-test-lws_struct-json/README.md
@@ -0,0 +1,56 @@
+# lws api test lws_struct JSON
+
+Demonstrates how to use and performs selftests for lws_struct
+JSON serialization and deserialization
+
+## build
+
+```
+ $ cmake . && make
+```
+
+## usage
+
+Commandline option|Meaning
+---|---
+-d <loglevel>|Debug verbosity in decimal, eg, -d15
+
+```
+ $ ./lws-api-test-lws_struct-json
+[2019/03/30 22:09:09:2529] USER: LWS API selftest: lws_struct JSON
+[2019/03/30 22:09:09:2625] NOTICE: main: ++++++++++++++++ test 1
+[2019/03/30 22:09:09:2812] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (2)
+[2019/03/30 22:09:09:2822] NOTICE:     target.name 'target1' (target 0x543a830)
+[2019/03/30 22:09:09:2824] NOTICE:     target.name 'target2' (target 0x543a860)
+[2019/03/30 22:09:09:2826] NOTICE: main:    .... strarting serialization of test 1
+[2019/03/30 22:09:09:2899] NOTICE: ser says 1
+{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800,"targets":[{"name":"target1"},{"name":"target2"}]}
+[2019/03/30 22:09:09:2929] NOTICE: main: ++++++++++++++++ test 2
+[2019/03/30 22:09:09:2932] NOTICE: builder.hostname = 'learn', timeout = 0, targets (3)
+[2019/03/30 22:09:09:2932] NOTICE:     target.name 'target1' (target 0x543b060)
+[2019/03/30 22:09:09:2933] NOTICE:     target.name 'target2' (target 0x543b090)
+[2019/03/30 22:09:09:2933] NOTICE:     target.name 'target3' (target 0x543b0c0)
+[2019/03/30 22:09:09:2934] NOTICE: main:    .... strarting serialization of test 2
+[2019/03/30 22:09:09:2935] NOTICE: ser says 1
+{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":0,"targets":[{"name":"target1"},{"name":"target2"},{"name":"target3"}]}
+[2019/03/30 22:09:09:2940] NOTICE: main: ++++++++++++++++ test 3
+[2019/03/30 22:09:09:2959] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (2)
+[2019/03/30 22:09:09:2960] NOTICE:     target.name 'target1' (target 0x543b450)
+[2019/03/30 22:09:09:2961] NOTICE:       child 0x543b480, target.child.somename 'abc'
+[2019/03/30 22:09:09:2961] NOTICE:     target.name 'target2' (target 0x543b490)
+[2019/03/30 22:09:09:2962] NOTICE: main:    .... strarting serialization of test 3
+[2019/03/30 22:09:09:2969] NOTICE: ser says 1
+{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800,"targets":[{"name":"target1","child":{"somename":"abc"}},{"name":"target2"}]}
+[2019/03/30 22:09:09:2970] NOTICE: main: ++++++++++++++++ test 4
+[2019/03/30 22:09:09:2971] NOTICE: builder.hostname = 'learn', timeout = 1800, targets (0)
+[2019/03/30 22:09:09:2971] NOTICE: main:    .... strarting serialization of test 4
+[2019/03/30 22:09:09:2973] NOTICE: ser says 1
+{"schema":"com-warmcat-sai-builder","hostname":"learn","nspawn_timeout":1800}
+[2019/03/30 22:09:09:2974] NOTICE: main: ++++++++++++++++ test 5
+[2019/03/30 22:09:09:2978] NOTICE: builder.hostname = '', timeout = 0, targets (0)
+[2019/03/30 22:09:09:2979] NOTICE: main:    .... strarting serialization of test 5
+[2019/03/30 22:09:09:2980] NOTICE: ser says 1
+{"schema":"com-warmcat-sai-builder","hostname":"","nspawn_timeout":0}
+[2019/03/30 22:09:09:2982] USER: Completed: PASS
+```
+
diff --git a/minimal-examples/api-tests/api-test-lws_struct-json/main.c b/minimal-examples/api-tests/api-test-lws_struct-json/main.c
new file mode 100644
index 0000000000000000000000000000000000000000..98c7d93ad9503f3a4ce40d5df71df392058b5509
--- /dev/null
+++ b/minimal-examples/api-tests/api-test-lws_struct-json/main.c
@@ -0,0 +1,365 @@
+/*
+ * lws-api-test-lws_struct-json
+ *
+ * Copyright (C) 2019 Andy Green <andy@warmcat.com>
+ *
+ * This file is made available under the Creative Commons CC0 1.0
+ * Universal Public Domain Dedication.
+ *
+ * lws_struct apis are used to serialize and deserialize your C structs and
+ * linked-lists in a standardized way that's very modest on memory but
+ * convenient and easy to maintain.
+ *
+ * The API test shows how to serialize and deserialize a struct with a linked-
+ * list of child structs in JSON using lws_struct APIs.
+ */
+
+#include <libwebsockets.h>
+
+/*
+ * in this example, the JSON is for one "builder" object, which may specify
+ * a child list "targets" of zero or more "target" objects.
+ */
+
+static const char * const json_tests[] = {
+	"{" /* test 1 */
+		"\"schema\":\"com-warmcat-sai-builder\","
+
+		"\"hostname\":\"learn\","
+		"\"nspawn_timeout\":1800,"
+		"\"targets\":["
+			"{"
+				"\"name\":\"target1\","
+				"\"someflag\":true"
+			"},"
+			"{"
+				"\"name\":\"target2\","
+				"\"someflag\":false"
+			"}"
+		"]"
+	"}",
+	"{" /* test 2 */
+		"\"schema\":\"com-warmcat-sai-builder\","
+
+		"\"hostname\":\"learn\","
+		"\"targets\":["
+			"{"
+				"\"name\":\"target1\""
+			"},"
+			"{"
+				"\"name\":\"target2\""
+			"},"
+			"{"
+				"\"name\":\"target3\""
+			"}"
+		"]"
+	"}", "{" /* test 3 */
+		"\"schema\":\"com-warmcat-sai-builder\","
+
+		"\"hostname\":\"learn\","
+		"\"nspawn_timeout\":1800,"
+		"\"targets\":["
+			"{"
+				"\"name\":\"target1\","
+				"\"unrecognized\":\"xyz\","
+				"\"child\": {"
+					"\"somename\": \"abc\","
+					"\"junk\": { \"x\": \"y\" }"
+				"}"
+			"},"
+			"{"
+				"\"name\":\"target2\""
+			"}"
+		"]"
+	"}",
+	"{" /* test 4 */
+		"\"schema\":\"com-warmcat-sai-builder\","
+
+		"\"hostname\":\"learn\","
+		"\"nspawn_timeout\":1800"
+	"}",
+	"{" /* test 5 */
+		"\"schema\":\"com-warmcat-sai-builder\""
+	"}",
+	"{" /* test 6 ... check huge strings into smaller fixed char array */
+		"\"schema\":\"com-warmcat-sai-builder\","
+		"\"hostname\":\""
+		"PYvtan6kqppjnS0KpYTCaiOLsJkc7XecAr1kcE0aCIciewYB+JcLG82mO1Vb1mJtjDwUjBxy2I6A"
+		"zefzoWUWmqZbsv4MXR55j9bKlyz1liiSX63iO0x6JAwACMtE2MkgcLwR86TSWAD9D1QKIWqg5RJ/"
+		"CRuVsW0DKAUMD52ql4JmPFuJpJgTq28z6PhYNzN3yI3bmQt6bzhA+A/xAsFzSBnb3MHYWzGMprr5"
+		"3FAP1ISo5Ec9i+2ehV40sG6Q470sH3PGQZ0YRPO7Sh/SyrSQ/scONmxRc3AcXl7X/CSs417ii+CV"
+		"8sq3ZgcxKNB7tNfN7idNx3upZ00G2BZy9jSy03cLKKLNaNUt0TQsxXbH55uDHzSEeZWvxJgT6zB1"
+		"NoMhdC02w+oXim94M6z6COCnqT3rgkGk8PHMry9Bkh4yVpRmzIRfMmln/lEhdZgxky2+g5hhlSIG"
+		"JYDCrdynD9kCfvfy6KGOpNIi1X+mhbbWn4lnL9ZKihL/RrfOV+oV4R26IDq+KqUiJBENeo8/GXkG"
+		"LUH/87iPyzXKEMavr6fkrK0vTGto8yEYxmOyaVz8phG5rwf4jJgmYNoMbGo8gWvhqO7UAGy2g7MW"
+		"v+B/t1eZZ+1euLsNrWAsFJiFbQKgdFfQT3RjB14iU8knlQ8usoy+pXssY2ddGJGVcGC21oZvstK9"
+		"eu1eRZftda/wP+N5unT1Hw7kCoVzqxHieiYt47EGIOaaQ7XjZDK6qPN6O/grHnvJZm2vBkxuXgsY"
+		"VkRQ7AuTWIecphqFsq7Wbc1YNbMW47SVU5zMD0WaCqbaaI0t4uIzRvPlD8cpiiTzFTrEHlIBTf8/"
+		"uZjjEGGLhJR1jPqA9D1Ej3ChV+ye6F9JTUMlozRMsGuF8U4btDzH5xdnmvRS4Ar6LKEtAXGkj2yu"
+		"yJln+v4RIWj2xOGPJovOqiXwi0FyM61f8U8gj0OiNA2/QlvrqQVDF7sMXgjvaE7iQt5vMETteZlx"
+		"+z3f+jTFM/aon511W4+ZkRD+6AHwucvM9BEC\""
+	"}",
+	"{" /* test 7 ... check huge strings into char * */
+		"\"schema\":\"com-warmcat-sai-builder\","
+		"\"targets\":["
+			"{"
+				"\"name\":\""
+		"PYvtan6kqppjnS0KpYTCaiOLsJkc7XecAr1kcE0aCIciewYB+JcLG82mO1Vb1mJtjDwUjBxy2I6A"
+		"zefzoWUWmqZbsv4MXR55j9bKlyz1liiSX63iO0x6JAwACMtE2MkgcLwR86TSWAD9D1QKIWqg5RJ/"
+		"CRuVsW0DKAUMD52ql4JmPFuJpJgTq28z6PhYNzN3yI3bmQt6bzhA+A/xAsFzSBnb3MHYWzGMprr5"
+		"3FAP1ISo5Ec9i+2ehV40sG6Q470sH3PGQZ0YRPO7Sh/SyrSQ/scONmxRc3AcXl7X/CSs417ii+CV"
+		"8sq3ZgcxKNB7tNfN7idNx3upZ00G2BZy9jSy03cLKKLNaNUt0TQsxXbH55uDHzSEeZWvxJgT6zB1"
+		"NoMhdC02w+oXim94M6z6COCnqT3rgkGk8PHMry9Bkh4yVpRmzIRfMmln/lEhdZgxky2+g5hhlSIG"
+		"JYDCrdynD9kCfvfy6KGOpNIi1X+mhbbWn4lnL9ZKihL/RrfOV+oV4R26IDq+KqUiJBENeo8/GXkG"
+		"LUH/87iPyzXKEMavr6fkrK0vTGto8yEYxmOyaVz8phG5rwf4jJgmYNoMbGo8gWvhqO7UAGy2g7MW"
+		"v+B/t1eZZ+1euLsNrWAsFJiFbQKgdFfQT3RjB14iU8knlQ8usoy+pXssY2ddGJGVcGC21oZvstK9"
+		"eu1eRZftda/wP+N5unT1Hw7kCoVzqxHieiYt47EGIOaaQ7XjZDK6qPN6O/grHnvJZm2vBkxuXgsY"
+		"VkRQ7AuTWIecphqFsq7Wbc1YNbMW47SVU5zMD0WaCqbaaI0t4uIzRvPlD8cpiiTzFTrEHlIBTf8/"
+		"uZjjEGGLhJR1jPqA9D1Ej3ChV+ye6F9JTUMlozRMsGuF8U4btDzH5xdnmvRS4Ar6LKEtAXGkj2yu"
+		"yJln+v4RIWj2xOGPJovOqiXwi0FyM61f8U8gj0OiNA2/QlvrqQVDF7sMXgjvaE7iQt5vMETteZlx"
+		"+z3f+jTFM/aon511W4+ZkRD+6AHwucvM9BEC\"}]}"
+	"}",
+};
+
+/*
+ * These are the expected outputs for each test, without pretty formatting.
+ *
+ * There are some differences to do with missing elements being rendered with
+ * default values.
+ */
+
+static const char * const json_expected[] = {
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":\"learn\","
+	  "\"nspawn_timeout\":1800,\"targets\":[{\"name\":\"target1\",\"someflag\":true},"
+	  "{\"name\":\"target2\",\"someflag\":false}]}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":\"learn\","
+	 "\"nspawn_timeout\":0,\"targets\":[{\"name\":\"target1\",\"someflag\":false},"
+	  "{\"name\":\"target2\",\"someflag\":false},{\"name\":\"target3\",\"someflag\":false}]}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":\"learn\","
+	"\"nspawn_timeout\":1800,\"targets\":[{\"name\":\"target1\",\"someflag\":false,"
+	  "\"child\":{\"somename\":\"abc\"}},{\"name\":\"target2\",\"someflag\":false}]}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\","
+	  "\"hostname\":\"learn\",\"nspawn_timeout\":1800}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":\"\","
+	"\"nspawn_timeout\":0}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":"
+		"\"PYvtan6kqppjnS0KpYTCaiOLsJkc7Xe\","
+	"\"nspawn_timeout\":0}",
+
+	"{\"schema\":\"com-warmcat-sai-builder\",\"hostname\":\"\","
+	  "\"nspawn_timeout\":0,\"targets\":[{\"name\":\"PYvtan6kqppjnS0KpYTC"
+		"aiOLsJkc7XecAr1kcE0aCIciewYB+JcLG82mO1Vb1mJtjDwUjBxy2I6Azefz"
+		"oWUWmqZbsv4MXR55j9bKlyz1liiSX63iO0x6JAwACMtE2MkgcLwR86TSWAD9"
+		"D1QKIWqg5RJ/CRuVsW0DKAUMD52ql4JmPFuJpJgTq28z6PhYNzN3yI3bmQt6"
+		"bzhA+A/xAsFzSBnb3MHYWzGMprr53FAP1ISo5Ec9i+2ehV40sG6Q470sH3PG"
+		"QZ0YRPO7Sh/SyrSQ/scONmxRc3AcXl7X/CSs417ii+CV8sq3ZgcxKNB7tNfN"
+		"7idNx3upZ00G2BZy9jSy03cLKKLNaNUt0TQsxXbH55uDHzSEeZWvxJgT6zB1"
+		"NoMhdC02w+oXim94M6z6COCnqT3rgkGk8PHMry9Bkh4yVpRmzIRfMmln/lEh"
+		"dZgxky2+g5hhlSIGJYDCrdynD9kCfvfy6KGOpNIi1X+mhbbWn4lnL9ZKihL/"
+		"RrfOV+oV4R26IDq+KqUiJBENeo8/GXkGLUH/87iPyzXKEMavr6fkrK0vTGto"
+		"8yEYxmOyaVz8phG5rwf4jJgmYNoMbGo8gWvhqO7UAGy2g7MWv+B/t1eZZ+1e"
+		"uLsNrWAsFJiFbQKgdFfQT3RjB14iU8knlQ8usoy+pXssY2ddGJGVcGC21oZv"
+		"stK9eu1eRZftda/wP+N5unT1Hw7kCoVzqxHieiYt47EGIOaaQ7XjZDK6qPN6"
+		"O/grHnvJZm2vBkxuXgsYVkRQ7AuTWIecphqFsq7Wbc1YNbMW47SVU5zMD0Wa"
+		"CqbaaI0t4uIzRvPlD8cpiiTzFTrEHlIBTf8/uZjjEGGLhJR1jPqA9D1Ej3Ch"
+		"V+ye6F9JTUMlozRMsGuF8U4btDzH5xdnmvRS4Ar6LKEtAXGkj2yuyJln+v4R"
+		"IWj2xOGPJovOqiXwi0FyM61f8U8gj0OiNA2/QlvrqQVDF7sMXgjvaE7iQt5v"
+		"METteZlx+z3f+jTFM/aon511W4+ZkRD+6AHwucvM9BEC\""
+			",\"someflag\":false}]}"
+};
+
+/*
+ * These annotate the members in the struct that will be serialized and
+ * deserialized with type and size information, as well as the name to use
+ * in the serialization format.
+ *
+ * Struct members that aren't annotated like this won't be serialized and
+ * when the struct is created during deserialiation, the will be set to 0
+ * or NULL.
+ */
+
+/* child object */
+
+typedef struct sai_child {
+	const char *	somename;
+} sai_child_t;
+
+lws_struct_map_t lsm_child[] = { /* describes serializable members */
+	LSM_STRING_PTR	(sai_child_t, somename,			"somename"),
+};
+
+/* target object */
+
+typedef struct sai_target {
+	struct lws_dll2 target_list;
+	sai_child_t *		child;
+
+	const char *		name;
+	char			someflag;
+} sai_target_t;
+
+static const lws_struct_map_t lsm_target[] = {
+	LSM_STRING_PTR	(sai_target_t, name,			"name"),
+	LSM_BOOLEAN	(sai_target_t, someflag,		"someflag"),
+	LSM_CHILD_PTR	(sai_target_t, child, sai_child_t,
+			 NULL, lsm_child,			"child"),
+};
+
+/* builder object */
+
+typedef struct sai_builder {
+	struct lws_dll2_owner	targets;
+
+	char 			hostname[32];
+	unsigned int 		nspawn_timeout;
+} sai_builder_t;
+
+static const lws_struct_map_t lsm_builder[] = {
+	LSM_CARRAY	(sai_builder_t, hostname,		"hostname"),
+	LSM_UNSIGNED	(sai_builder_t, nspawn_timeout,		"nspawn_timeout"),
+	LSM_LIST	(sai_builder_t, targets,
+			 sai_target_t, target_list,
+			 NULL, lsm_target,			"targets"),
+};
+
+/* Schema table
+ *
+ * Before we can understand the serialization top level format, we must read
+ * the schema, use the table below to create the right toplevel object for the
+ * schema name, and select the correct map tables to interpret the rest of the
+ * serialization.
+ *
+ * Therefore the schema tables below are the starting point for the
+ * JSON deserialization.
+ */
+
+static const lws_struct_map_t lsm_schema_map[] = {
+	LSM_SCHEMA	(sai_builder_t, NULL,
+			 lsm_builder,		"com-warmcat-sai-builder"),
+};
+
+static int
+show_target(struct lws_dll2 *d, void *user)
+{
+	sai_target_t *t = lws_container_of(d, sai_target_t, target_list);
+
+	lwsl_notice("    target.name '%s' (target %p)\n", t->name, t);
+
+	if (t->child)
+		lwsl_notice("      child %p, target.child.somename '%s'\n",
+			  t->child, t->child->somename);
+
+	return 0;
+}
+
+
+int main(int argc, const char **argv)
+{
+	int n, m, e = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE;
+#if 1
+	lws_struct_serialize_t *ser;
+	uint8_t buf[4096];
+	size_t written;
+#endif
+	struct lejp_ctx ctx;
+	lws_struct_args_t a;
+	sai_builder_t *b;
+	const char *p;
+
+	if ((p = lws_cmdline_option(argc, argv, "-d")))
+		logs = atoi(p);
+
+	lws_set_log_level(logs, NULL);
+	lwsl_user("LWS API selftest: lws_struct JSON\n");
+
+	for (m = 0; m < (int)LWS_ARRAY_SIZE(json_tests); m++) {
+
+		/* 1. deserialize the canned JSON into structs */
+
+		lwsl_notice("%s: ++++++++++++++++ test %d\n", __func__, m + 1);
+
+		memset(&a, 0, sizeof(a));
+		a.map_st[0] = lsm_schema_map;
+		a.map_entries_st[0] = LWS_ARRAY_SIZE(lsm_schema_map);
+		a.ac_block_size = 512;
+
+		lws_struct_json_init_parse(&ctx, NULL, &a);
+		n = (int)(signed char)lejp_parse(&ctx, (uint8_t *)json_tests[m],
+						 strlen(json_tests[m]));
+		if (n < 0) {
+			lwsl_err("%s: notification JSON decode failed '%s'\n",
+					__func__, lejp_error_to_string(n));
+			e++;
+			goto done;
+		}
+		lwsac_info(a.ac);
+
+		b = a.dest;
+		if (!b) {
+			lwsl_err("%s: didn't produce any output\n", __func__);
+			e++;
+			goto done;
+		}
+
+		lwsl_notice("builder.hostname = '%s', timeout = %d, targets (%d)\n",
+			    b->hostname, b->nspawn_timeout,
+			    b->targets.count);
+
+		lws_dll2_foreach_safe(&b->targets, NULL, show_target);
+
+		/* 2. serialize the structs into JSON and confirm */
+
+		lwsl_notice("%s:    .... strarting serialization of test %d\n",
+				__func__, m + 1);
+		ser = lws_struct_json_serialize_create(lsm_schema_map,
+						LWS_ARRAY_SIZE(lsm_schema_map),
+						       0//LSSERJ_FLAG_PRETTY
+						       , b);
+		if (!ser) {
+			lwsl_err("%s: unable to init serialization\n", __func__);
+			goto bail;
+		}
+
+		do {
+			n = lws_struct_json_serialize(ser, buf, sizeof(buf),
+						      &written);
+			lwsl_notice("ser says %d\n", n);
+			switch (n) {
+			case LSJS_RESULT_CONTINUE:
+			case LSJS_RESULT_FINISH:
+				puts((const char *)buf);
+				break;
+			case LSJS_RESULT_ERROR:
+				goto bail;
+			}
+		} while(n == LSJS_RESULT_CONTINUE);
+
+		if (strcmp(json_expected[m], (char *)buf)) {
+			lwsl_err("%s: test %d: expected %s\n", __func__, m + 1,
+					json_expected[m]);
+			e++;
+		}
+
+		lws_struct_json_serialize_destroy(&ser);
+
+done:
+		lwsac_free(&a.ac);
+	}
+
+	if (e)
+		goto bail;
+
+	lwsl_user("Completed: PASS\n");
+
+	return 0;
+
+bail:
+	lwsl_user("Completed: FAIL\n");
+
+	return 1;
+}
diff --git a/minimal-examples/api-tests/api-test-lws_struct-json/selftest.sh b/minimal-examples/api-tests/api-test-lws_struct-json/selftest.sh
new file mode 100755
index 0000000000000000000000000000000000000000..16d1e2e8e463dec610c1cc4c2c7cc22f3cf0b14f
--- /dev/null
+++ b/minimal-examples/api-tests/api-test-lws_struct-json/selftest.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+# $1: path to minimal example binaries...
+#     if lws is built with -DLWS_WITH_MINIMAL_EXAMPLES=1
+#     that will be ./bin from your build dir
+#
+# $2: path for logs and results.  The results will go
+#     in a subdir named after the directory this script
+#     is in
+#
+# $3: offset for test index count
+#
+# $4: total test count
+#
+# $5: path to ./minimal-examples dir in lws
+#
+# Test return code 0: OK, 254: timed out, other: error indication
+
+. $5/selftests-library.sh
+
+COUNT_TESTS=1
+
+dotest $1 $2 apiselftest
+exit $FAILS