This is an Advent of Code solution. Click to reveal!
The cogs are turning...
    
https://zigbin.io/159568Click to copy
https://zigbin.io/159568/runClick to copy
https://zigbin.io/159568/rawClick to copy
const std = @import("std");

pub const Tlv = struct {
    tag: u8,
    value: []const u8,

    pub fn init(tag: u8, value: []const u8) Tlv {
        return .{ .tag = tag, .value = value };
    }

    pub fn write(tlv: Tlv, writer: anytype) !void {
        try writer.writeByte(tlv.tag);
        try writer.writeByte(@intCast(tlv.value.len));
        _ = try writer.write(tlv.value);
    }

    pub fn encode(tlvs: []const Tlv, allocator: std.mem.Allocator) ![]const u8 {
        var buf = std.ArrayList(u8).init(allocator);
        defer buf.deinit();
        const writer = buf.writer();
        for (tlvs) |tlv| {
            try tlv.write(writer);
        }
        const E = std.base64.standard.Encoder;
        const encoded = try allocator.alloc(u8, E.calcSize(buf.items.len));
        _ = E.encode(encoded, buf.items);
        return encoded;
    }

    pub fn decode(encoded: []const u8, allocator: std.mem.Allocator) !struct { tlvs: std.ArrayList(Tlv), decoded: []u8 } {
        var E = std.base64.standard.Decoder;
        const decodedLen = try E.calcSizeForSlice(encoded);
        const decoded = try allocator.alloc(u8, decodedLen);
        defer allocator.free(decoded);
        try E.decode(decoded, encoded);
        var tlvs = std.ArrayList(Tlv).init(allocator);
        var i: usize = 0;
        while (i < decoded.len-1) {
             const len = decoded[i+1];
        //     if (i + 2 + len > decoded.len) {
        //         break;
        //     }
            try tlvs.append(.{.tag = decoded[i], .value = decoded[i+2..][0..len] });
            i += 2 + len;
        }
        return .{ .tlvs = tlvs, .decoded = decoded };
    }
};

pub const Tlvs = struct {
    items: []const Tlv,

    pub fn tlvsToJson(self: Tlvs, allocator: std.mem.Allocator) ![]u8 {
        var json = try allocator.alloc(u8, 2);
        json[0] = '[';
        json[1] = ']';
        var i: usize = 0;
        while (i < self.items.len) {
            const tlv = self.items[i];
            if (i == 0) {
                json = try std.fmt.allocPrint(allocator, "{s}{{\"tag\": {d}, \"value\": \"{s}\"}}", .{json[0..json.len-1], tlv.tag, tlv.value});
            } else {
                json = try std.fmt.allocPrint(allocator, "{s},{{\"tag\": {d}, \"value\": \"{s}\"}}", .{json[0..json.len-1], tlv.tag, tlv.value});
            }
            i += 1;
        }
        json = try std.fmt.allocPrint(allocator, "{s}]", .{json});
        return json;
    }
};

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const allocator = arena.allocator();
    const tlvs = [_]Tlv{
        Tlv.init(1, "Bonz carpet"),
        Tlv.init(2, "310122393500003"),
        Tlv.init(3, "2022-04-25T15:30:00Z"),
        Tlv.init(4, "1200.00"),
        Tlv.init(5, "180.00"),
    };
    const encoded = try Tlv.encode(&tlvs, allocator);
    defer allocator.free(encoded);
    std.debug.print("Base64: {s}\n", .{ encoded});
	
	const result = try Tlv.decode(encoded, allocator);
	for (result.tlvs.items) |tlv| {
	    std.debug.print("TLV.tagNum = {any}, TLV.tagName = {s}\n", .{tlv.tag, tlv.value});
	}
	
	std.debug.print("Decoded: {s}\n", .{std.mem.bytesAsSlice(u8, result.decoded)});
	
    const tlvs2 = Tlvs{ .items = result.tlvs.items };
    const json = try tlvs2.tlvsToJson(allocator);
    std.debug.print("{s}\n", .{json});

    // Free the memory allocated for the JSON string
    allocator.free(json);
        
    // Free the memory allocated for tlvs and decoded
    result.tlvs.deinit();
    allocator.free(result.decoded);
}