diff --git a/eeschema/bom_plugins.cpp b/eeschema/bom_plugins.cpp
index fff25c7e25..86f444dafd 100644
--- a/eeschema/bom_plugins.cpp
+++ b/eeschema/bom_plugins.cpp
@@ -23,14 +23,23 @@
  */
 
 #include "bom_plugins.h"
+#include <paths.h>
 #include <wx/ffile.h>
+#include <wx/log.h>
+
+
+constexpr wxChar BOM_TRACE[] = wxT( "BOM_GENERATORS" );
+
 
 BOM_GENERATOR_HANDLER::BOM_GENERATOR_HANDLER( const wxString& aFile )
     : m_file( aFile )
 {
     m_isOk = false;
 
-    if( !wxFile::Exists( aFile ) )
+    if( !wxFile::Exists( m_file.GetFullPath() ) )
+        m_file = FindFilePath();
+
+    if( !wxFile::Exists( m_file.GetFullPath() ) )
     {
         m_info.Printf( _("Script file:\n%s\nnot found. Script not available."), aFile );
         return;
@@ -77,6 +86,8 @@ BOM_GENERATOR_HANDLER::BOM_GENERATOR_HANDLER( const wxString& aFile )
     {
         m_cmd = m_file.GetFullPath();
     }
+
+    wxLogTrace( BOM_TRACE, "%s: extracted command line %s", m_name, m_cmd );
 }
 
 
@@ -147,3 +158,36 @@ wxString BOM_GENERATOR_HANDLER::getOutputExtension( const wxString& aHeader )
 
     return aHeader.SubString( strstart, strend - 1 );
 }
+
+
+wxFileName BOM_GENERATOR_HANDLER::FindFilePath() const
+{
+    if( m_file.IsAbsolute() && m_file.Exists( wxFILE_EXISTS_REGULAR ) )
+    {
+        wxLogTrace( BOM_TRACE, "%s found directly", m_file.GetFullPath() );
+        return m_file;
+    }
+
+    wxFileName test( PATHS::GetUserPluginsPath(), m_file.GetName(), m_file.GetExt() );
+
+    if( test.Exists( wxFILE_EXISTS_REGULAR ) )
+    {
+        wxLogTrace( BOM_TRACE, "%s found in user plugins path %s", m_file.GetFullName(),
+                    PATHS::GetUserPluginsPath() );
+        return test;
+    }
+
+    test = wxFileName( PATHS::GetStockPluginsPath(), m_file.GetName(), m_file.GetExt() );
+
+    if( test.Exists( wxFILE_EXISTS_REGULAR ) )
+    {
+        wxLogTrace( BOM_TRACE, "%s found in stock plugins path %s", m_file.GetFullName(),
+                    PATHS::GetStockPluginsPath() );
+        return test;
+    }
+
+    wxLogTrace( BOM_TRACE, "Could not find %s (checked %s, %s)", m_file.GetFullName(),
+                PATHS::GetUserPluginsPath(), PATHS::GetStockPluginsPath() );
+
+    return m_file;
+}
diff --git a/eeschema/bom_plugins.h b/eeschema/bom_plugins.h
index a14b035189..32f46f4759 100644
--- a/eeschema/bom_plugins.h
+++ b/eeschema/bom_plugins.h
@@ -33,6 +33,8 @@
 
 #include <memory>
 
+extern const wxChar BOM_TRACE[];
+
 /**
  * Bill of material output generator.
  *
@@ -78,6 +80,16 @@ public:
         return m_file;
     }
 
+    /**
+     * Returns the calculated path to the plugin: if the path is already absolute and exists,
+     * just return it.  Otherwise if the path is just a filename, look for that file in the user
+     * and system plugin directories and return the first one found. If neither is found, just
+     * return m_file.
+     *
+     * @return the full path to the plugin
+     */
+    wxFileName FindFilePath() const;
+
     /**
      * Return the customisable plugin name.
      */
@@ -140,7 +152,7 @@ protected:
     bool m_isOk;
 
     ///< Path to the plugin
-    const wxFileName m_file;
+    wxFileName m_file;
 
     ///< User customisable name
     wxString m_name;
diff --git a/eeschema/dialogs/dialog_bom.cpp b/eeschema/dialogs/dialog_bom.cpp
index f5604b21d0..b175ae6655 100644
--- a/eeschema/dialogs/dialog_bom.cpp
+++ b/eeschema/dialogs/dialog_bom.cpp
@@ -46,131 +46,18 @@
 #include <schematic.h>
 #include <paths.h>
 
-#include <dialogs/dialog_bom_cfg_lexer.h>
-
 #include <wx/filedlg.h>
 #include <wx/textdlg.h>
 
-static constexpr wxChar BOM_TRACE[] = wxT( "BOM_GENERATORS" );
-
 wxString s_bomHelpInfo =
 #include <dialog_bom_help_md.h>
 ;
 
-using namespace T_BOMCFG_T;     // for the BOM_CFG_PARSER parser and its keywords
-
 // BOM "plugins" are not actually plugins. They are external tools
 // (scripts or executables) called by this dialog.
 typedef std::vector<BOM_GENERATOR_HANDLER::PTR> BOM_GENERATOR_ARRAY;
 
 
-/**
- * Holds data and functions pertinent to parsing a S-expression file
- */
-class BOM_CFG_PARSER : public DIALOG_BOM_CFG_LEXER
-{
-    BOM_GENERATOR_ARRAY* m_generatorsList;
-
-public:
-    BOM_CFG_PARSER( BOM_GENERATOR_ARRAY* aGenerators, const char* aData, const wxString& aSource );
-    void Parse();
-
-private:
-    void parseGenerator();
-};
-
-
-BOM_CFG_PARSER::BOM_CFG_PARSER( BOM_GENERATOR_ARRAY* aGenerators, const char* aLine,
-                                const wxString& aSource ) :
-    DIALOG_BOM_CFG_LEXER( aLine, aSource )
-{
-    m_generatorsList = aGenerators;
-}
-
-
-void BOM_CFG_PARSER::Parse()
-{
-    T token;
-
-    while( ( token = NextTok() ) != T_RIGHT )
-    {
-        if( token == T_EOF)
-           break;
-
-        if( token == T_LEFT )
-            token = NextTok();
-
-        if( token == T_plugins )
-            continue;
-
-        switch( token )
-        {
-        case T_plugin:   // Defines a new plugin
-            parseGenerator();
-            break;
-
-        default:
-//            Unexpected( CurText() );
-            break;
-        }
-    }
-}
-
-
-void BOM_CFG_PARSER::parseGenerator()
-{
-    NeedSYMBOLorNUMBER();
-    wxString name = FromUTF8();
-    auto plugin = std::make_unique<BOM_GENERATOR_HANDLER>( name );
-
-    T token;
-
-    while( ( token = NextTok() ) != T_RIGHT )
-    {
-        if( token == T_EOF)
-           break;
-
-        switch( token )
-        {
-        case T_LEFT:
-            break;
-
-        case T_cmd:
-            NeedSYMBOLorNUMBER();
-
-            if( plugin )
-                plugin->SetCommand( FromUTF8() );
-
-            NeedRIGHT();
-            break;
-
-        case T_opts:
-            NeedSYMBOLorNUMBER();
-
-            if( plugin )
-            {
-                wxString option = FromUTF8();
-
-                if( option.StartsWith( "nickname=", &name ) )
-                    plugin->SetName( name );
-                else
-                    plugin->Options().Add( option );
-            }
-
-            NeedRIGHT();
-            break;
-
-        default:
-            Unexpected( CurText() );
-            break;
-        }
-    }
-
-    if( plugin )
-        m_generatorsList->push_back( std::move( plugin ) );
-}
-
-
 // The main dialog frame to run scripts to build bom
 class DIALOG_BOM : public DIALOG_BOM_BASE
 {
@@ -251,56 +138,44 @@ DIALOG_BOM::DIALOG_BOM( SCH_EDIT_FRAME* parent ) :
 
     // Now all widgets have the size fixed, call FinishDialogSettings
     finishDialogSettings();
+
+    m_buttonReset->Bind( wxEVT_BUTTON,
+            [&]( wxCommandEvent& )
+            {
+                EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
+
+                cfg->m_BomPanel.selected_plugin = wxEmptyString;
+                cfg->m_BomPanel.plugins         = cfg->DefaultBomPlugins();
+
+                installGeneratorsList();
+            } );
 }
 
+
 DIALOG_BOM::~DIALOG_BOM()
 {
     if( m_helpWindow )
         m_helpWindow->Destroy();
 
-    // TODO(JE) maybe unpack this into JSON instead of sexpr
+    EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
 
-    // Save the plugin descriptions in config.
-    // The config stores only one string, so we save the plugins inside a S-expr:
-    // ( plugins
-    //    ( plugin "plugin name 1" (cmd "command line 1") )
-    //    ( plugin "plugin name 2" (cmd "command line 2") (opts "option1") (opts "option2") )
-    //     ....
-    // )
+    cfg->m_BomPanel.plugins.clear();
 
-    STRING_FORMATTER writer;
-    writer.Print( 0, "(plugins" );
-
-    for( auto& plugin : m_generators )
+    for( const std::unique_ptr<BOM_GENERATOR_HANDLER>& plugin : m_generators )
     {
-        writer.Print( 1, "(plugin %s (cmd %s)",
-                      writer.Quotew( plugin->GetFile().GetFullPath() ).c_str(),
-                      writer.Quotew( plugin->GetCommand() ).c_str() );
+        wxString   name = plugin->GetName();
+        wxFileName path = plugin->GetFile();
 
-        for( unsigned jj = 0; jj < plugin->Options().GetCount(); jj++ )
-        {
-            writer.Print( 1, "(opts %s)",
-                          writer.Quotew( plugin->Options().Item( jj ) ).c_str() );
-        }
+        // handle empty nickname by stripping path
+        if( name.IsEmpty() )
+            name = path.GetName();
 
-        if( !plugin->GetName().IsEmpty() )
-        {
-            wxString option = wxString::Format( "nickname=%s", plugin->GetName() );
+        EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS setting( name, path.GetFullPath() );
+        setting.command = plugin->GetCommand();
 
-            writer.Print( 1, "(opts %s)",
-                          writer.Quotew( option ).c_str() );
-        }
-
-        writer.Print( 0, ")" );
+        cfg->m_BomPanel.plugins.emplace_back( setting );
     }
 
-    writer.Print( 0, ")" );
-
-    wxString list( FROM_UTF8( writer.GetString().c_str() ) );
-
-    auto cfg = static_cast<EESCHEMA_SETTINGS*>( Kiface().KifaceSettings() );
-
-    cfg->m_BomPanel.plugins = list.ToStdString();
     cfg->m_BomPanel.selected_plugin = m_lbGenerators->GetStringSelection().ToStdString();
 }
 
@@ -308,33 +183,36 @@ DIALOG_BOM::~DIALOG_BOM()
 // Read the initialized plugins in config and fill the list of names
 void DIALOG_BOM::installGeneratorsList()
 {
-    auto cfg = static_cast<EESCHEMA_SETTINGS*>( Kiface().KifaceSettings() );
+    EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
 
-    wxString list               = cfg->m_BomPanel.plugins;
     wxString active_plugin_name = cfg->m_BomPanel.selected_plugin;
 
-    if( !list.IsEmpty() )
+    m_generators.clear();
+
+    for( EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS& setting : cfg->m_BomPanel.plugins )
     {
-        BOM_CFG_PARSER cfg_parser( &m_generators, TO_UTF8( list ), wxT( "plugins" ) );
+        auto plugin = std::make_unique<BOM_GENERATOR_HANDLER>( setting.path );
 
-        try
-        {
-            cfg_parser.Parse();
-        }
-        catch( const IO_ERROR& )
-        {
-//            wxLogMessage( ioe.What() );
-        }
-        catch( std::runtime_error& e )
-        {
-            DisplayError( nullptr, e.what() );
-        }
+        plugin->SetName( setting.name );
 
-        // Populate list box
+        if( !setting.command.IsEmpty() )
+            plugin->SetCommand( setting.command );
+
+        m_generators.emplace_back( std::move( plugin ) );
+    }
+
+    m_lbGenerators->Clear();
+
+    if( !m_generators.empty() )
+    {
         for( unsigned ii = 0; ii < m_generators.size(); ii++ )
         {
-            if( !m_generators[ii]->GetFile().Exists( wxFILE_EXISTS_REGULAR ) )
+            if( !m_generators[ii]->FindFilePath().Exists( wxFILE_EXISTS_REGULAR ) )
+            {
+                wxLogTrace( BOM_TRACE, "BOM plugin %s not found",
+                            m_generators[ii]->FindFilePath().GetFullName() );
                 continue;
+            }
 
             m_lbGenerators->Append( m_generators[ii]->GetName() );
 
@@ -343,53 +221,6 @@ void DIALOG_BOM::installGeneratorsList()
         }
     }
 
-    if( m_generators.empty() ) // No plugins found?
-    {
-        // Load plugins from the default locations
-        std::vector<wxString> pluginPaths = {
-#if defined(__WXGTK__)
-            "/usr/share/kicad/plugins",
-            "/usr/local/share/kicad/plugins",
-#elif defined(__WXMSW__)
-            wxString::Format( "%s\\scripting\\plugins", Pgm().GetExecutablePath() ),
-#elif defined(__WXMAC__)
-            wxString::Format( "%s/plugins", PATHS::GetOSXKicadDataDir() ),
-#endif
-        };
-
-        wxFileName pluginPath;
-
-        for( const auto& path : pluginPaths )
-        {
-            wxDir dir( path );
-
-            if( !dir.IsOpened() )
-                continue;
-
-            pluginPath.AssignDir( dir.GetName() );
-            wxString fileName;
-            bool cont = dir.GetFirst( &fileName, wxFileSelectorDefaultWildcardStr, wxDIR_FILES );
-
-            while( cont )
-            {
-                try
-                {
-                    wxLogTrace( BOM_TRACE,"Checking if %s is a BOM generator", fileName );
-
-                    if( BOM_GENERATOR_HANDLER::IsValidGenerator( fileName ) )
-                    {
-                        pluginPath.SetFullName( fileName );
-                        addGenerator( pluginPath.GetFullPath() );
-                    }
-                }
-                catch( ... ) { /* well, no big deal */ }
-
-                cont = dir.GetNext( &fileName );
-            }
-        }
-    }
-
-
     pluginInit();
 }
 
diff --git a/eeschema/dialogs/dialog_bom_base.cpp b/eeschema/dialogs/dialog_bom_base.cpp
index 0a15347995..4e9f017b57 100644
--- a/eeschema/dialogs/dialog_bom_base.cpp
+++ b/eeschema/dialogs/dialog_bom_base.cpp
@@ -109,6 +109,17 @@ DIALOG_BOM_BASE::DIALOG_BOM_BASE( wxWindow* parent, wxWindowID id, const wxStrin
 	m_staticline = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
 	bMainSizer->Add( m_staticline, 0, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 5 );
 
+	wxBoxSizer* bSizer8;
+	bSizer8 = new wxBoxSizer( wxHORIZONTAL );
+
+	m_buttonReset = new wxButton( this, wxID_ANY, _("Reset to Defaults"), wxDefaultPosition, wxDefaultSize, 0 );
+	m_buttonReset->SetToolTip( _("Reset the list of BOM generator scripts to the default settings") );
+
+	bSizer8->Add( m_buttonReset, 0, wxALL, 5 );
+
+
+	bSizer8->Add( 0, 0, 1, wxEXPAND, 5 );
+
 	m_sdbSizer = new wxStdDialogButtonSizer();
 	m_sdbSizerOK = new wxButton( this, wxID_OK );
 	m_sdbSizer->AddButton( m_sdbSizerOK );
@@ -118,7 +129,10 @@ DIALOG_BOM_BASE::DIALOG_BOM_BASE( wxWindow* parent, wxWindowID id, const wxStrin
 	m_sdbSizer->AddButton( m_sdbSizerHelp );
 	m_sdbSizer->Realize();
 
-	bMainSizer->Add( m_sdbSizer, 0, wxEXPAND|wxALL, 5 );
+	bSizer8->Add( m_sdbSizer, 0, wxEXPAND|wxALL, 5 );
+
+
+	bMainSizer->Add( bSizer8, 0, wxEXPAND, 5 );
 
 
 	this->SetSizer( bMainSizer );
diff --git a/eeschema/dialogs/dialog_bom_base.fbp b/eeschema/dialogs/dialog_bom_base.fbp
index 7d5900ae96..d032e248e1 100644
--- a/eeschema/dialogs/dialog_bom_base.fbp
+++ b/eeschema/dialogs/dialog_bom_base.fbp
@@ -920,22 +920,115 @@
                 </object>
                 <object class="sizeritem" expanded="1">
                     <property name="border">5</property>
-                    <property name="flag">wxEXPAND|wxALL</property>
+                    <property name="flag">wxEXPAND</property>
                     <property name="proportion">0</property>
-                    <object class="wxStdDialogButtonSizer" expanded="1">
-                        <property name="Apply">0</property>
-                        <property name="Cancel">1</property>
-                        <property name="ContextHelp">0</property>
-                        <property name="Help">1</property>
-                        <property name="No">0</property>
-                        <property name="OK">1</property>
-                        <property name="Save">0</property>
-                        <property name="Yes">0</property>
+                    <object class="wxBoxSizer" expanded="1">
                         <property name="minimum_size"></property>
-                        <property name="name">m_sdbSizer</property>
-                        <property name="permission">protected</property>
-                        <event name="OnHelpButtonClick">OnHelp</event>
-                        <event name="OnOKButtonClick">OnRunGenerator</event>
+                        <property name="name">bSizer8</property>
+                        <property name="orient">wxHORIZONTAL</property>
+                        <property name="permission">none</property>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxButton" expanded="1">
+                                <property name="BottomDockable">1</property>
+                                <property name="LeftDockable">1</property>
+                                <property name="RightDockable">1</property>
+                                <property name="TopDockable">1</property>
+                                <property name="aui_layer"></property>
+                                <property name="aui_name"></property>
+                                <property name="aui_position"></property>
+                                <property name="aui_row"></property>
+                                <property name="best_size"></property>
+                                <property name="bg"></property>
+                                <property name="bitmap"></property>
+                                <property name="caption"></property>
+                                <property name="caption_visible">1</property>
+                                <property name="center_pane">0</property>
+                                <property name="close_button">1</property>
+                                <property name="context_help"></property>
+                                <property name="context_menu">1</property>
+                                <property name="current"></property>
+                                <property name="default">0</property>
+                                <property name="default_pane">0</property>
+                                <property name="disabled"></property>
+                                <property name="dock">Dock</property>
+                                <property name="dock_fixed">0</property>
+                                <property name="docking">Left</property>
+                                <property name="enabled">1</property>
+                                <property name="fg"></property>
+                                <property name="floatable">1</property>
+                                <property name="focus"></property>
+                                <property name="font"></property>
+                                <property name="gripper">0</property>
+                                <property name="hidden">0</property>
+                                <property name="id">wxID_ANY</property>
+                                <property name="label">Reset to Defaults</property>
+                                <property name="margins"></property>
+                                <property name="markup">0</property>
+                                <property name="max_size"></property>
+                                <property name="maximize_button">0</property>
+                                <property name="maximum_size"></property>
+                                <property name="min_size"></property>
+                                <property name="minimize_button">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="moveable">1</property>
+                                <property name="name">m_buttonReset</property>
+                                <property name="pane_border">1</property>
+                                <property name="pane_position"></property>
+                                <property name="pane_size"></property>
+                                <property name="permission">protected</property>
+                                <property name="pin_button">1</property>
+                                <property name="pos"></property>
+                                <property name="position"></property>
+                                <property name="pressed"></property>
+                                <property name="resize">Resizable</property>
+                                <property name="show">1</property>
+                                <property name="size"></property>
+                                <property name="style"></property>
+                                <property name="subclass">; ; forward_declare</property>
+                                <property name="toolbar_pane">0</property>
+                                <property name="tooltip">Reset the list of BOM generator scripts to the default settings</property>
+                                <property name="validator_data_type"></property>
+                                <property name="validator_style">wxFILTER_NONE</property>
+                                <property name="validator_type">wxDefaultValidator</property>
+                                <property name="validator_variable"></property>
+                                <property name="window_extra_style"></property>
+                                <property name="window_name"></property>
+                                <property name="window_style"></property>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND</property>
+                            <property name="proportion">1</property>
+                            <object class="spacer" expanded="1">
+                                <property name="height">0</property>
+                                <property name="permission">protected</property>
+                                <property name="width">0</property>
+                            </object>
+                        </object>
+                        <object class="sizeritem" expanded="1">
+                            <property name="border">5</property>
+                            <property name="flag">wxEXPAND|wxALL</property>
+                            <property name="proportion">0</property>
+                            <object class="wxStdDialogButtonSizer" expanded="1">
+                                <property name="Apply">0</property>
+                                <property name="Cancel">1</property>
+                                <property name="ContextHelp">0</property>
+                                <property name="Help">1</property>
+                                <property name="No">0</property>
+                                <property name="OK">1</property>
+                                <property name="Save">0</property>
+                                <property name="Yes">0</property>
+                                <property name="minimum_size"></property>
+                                <property name="name">m_sdbSizer</property>
+                                <property name="permission">protected</property>
+                                <event name="OnHelpButtonClick">OnHelp</event>
+                                <event name="OnOKButtonClick">OnRunGenerator</event>
+                            </object>
+                        </object>
                     </object>
                 </object>
             </object>
diff --git a/eeschema/dialogs/dialog_bom_base.h b/eeschema/dialogs/dialog_bom_base.h
index 581ffa587e..00c3a3ba73 100644
--- a/eeschema/dialogs/dialog_bom_base.h
+++ b/eeschema/dialogs/dialog_bom_base.h
@@ -57,6 +57,7 @@ class DIALOG_BOM_BASE : public DIALOG_SHIM
 		wxTextCtrl* m_textCtrlCommand;
 		wxCheckBox* m_checkBoxShowConsole;
 		wxStaticLine* m_staticline;
+		wxButton* m_buttonReset;
 		wxStdDialogButtonSizer* m_sdbSizer;
 		wxButton* m_sdbSizerOK;
 		wxButton* m_sdbSizerCancel;
diff --git a/eeschema/eeschema_settings.cpp b/eeschema/eeschema_settings.cpp
index 2e34a0ab9a..41c4ee4029 100644
--- a/eeschema/eeschema_settings.cpp
+++ b/eeschema/eeschema_settings.cpp
@@ -21,6 +21,9 @@
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */
 
+#include <functional>
+
+#include <dialogs/dialog_bom_cfg_lexer.h>
 #include <eeschema_settings.h>
 #include <layers_id_colors_and_visibility.h>
 #include <symbol_editor_settings.h>
@@ -32,8 +35,22 @@
 #include <widgets/ui_common.h>
 #include <default_values.h>    // For some default values
 
+using namespace T_BOMCFG_T;     // for the BOM_CFG_PARSER parser and its keywords
+
 ///! Update the schema version whenever a migration is required
-const int eeschemaSchemaVersion = 0;
+const int eeschemaSchemaVersion = 1;
+
+/// Default value for bom.plugins
+const nlohmann::json defaultBomPlugins = {
+        {
+            { "name", "bom_csv_grouped_by_value" },
+            { "path", "bom_csv_grouped_by_value.py" }
+        },
+        {
+            { "name", "bom_csv_grouped_by_value_with_fp" },
+            { "path", "bom_csv_grouped_by_value_with_fp.py" }
+        },
+    };
 
 
 EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() :
@@ -184,8 +201,18 @@ EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() :
     m_params.emplace_back( new PARAM<wxString>( "bom.selected_plugin",
             &m_BomPanel.selected_plugin, "" ) );
 
-    m_params.emplace_back( new PARAM<wxString>( "bom.plugins",
-            &m_BomPanel.plugins, "" ) );
+    m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "bom.plugins",
+            std::bind( &EESCHEMA_SETTINGS::bomSettingsToJson, this ),
+            [&]( const nlohmann::json& aObj )
+            {
+                if( !aObj.is_array() )
+                    return;
+
+                const nlohmann::json& list = aObj.empty() ? defaultBomPlugins : aObj;
+
+                m_BomPanel.plugins = bomSettingsFromJson( list );
+            },
+            defaultBomPlugins ) );
 
     m_params.emplace_back( new PARAM<bool>( "page_settings.export_paper",
             &m_PageSettings.export_paper, false ) );
@@ -352,6 +379,15 @@ EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() :
     m_params.emplace_back( new PARAM<wxString>( "system.last_symbol_lib_dir",
             &m_lastSymbolLibDir, "" ) );
 
+
+    // Migrations
+
+    registerMigration( 0, 1,
+            [&]() -> bool
+            {
+                // Version 0 to 1: BOM plugin settings moved from sexpr to JSON
+                return migrateBomSettings();
+            } );
 }
 
 
@@ -415,6 +451,8 @@ bool EESCHEMA_SETTINGS::MigrateFromLegacy( wxConfigBase* aCfg )
     ret &= fromLegacyString( aCfg, "bom_plugin_selected",     "bom.selected_plugin" );
     ret &= fromLegacyString( aCfg, "bom_plugins",             "bom.plugins" );
 
+    migrateBomSettings();
+
     ret &= fromLegacyString( aCfg, "SymbolFieldsShownColumns",
             "edit_sch_component.visible_columns" );
 
@@ -595,3 +633,191 @@ bool EESCHEMA_SETTINGS::MigrateFromLegacy( wxConfigBase* aCfg )
 
     return ret;
 }
+
+
+/**
+ * Used for parsing legacy-format bom plugin configurations.  Only used for migrating into
+ * EESCHEMA_SETTINGS JSON format.
+ */
+class BOM_CFG_PARSER : public DIALOG_BOM_CFG_LEXER
+{
+    std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS>* m_pluginList;
+
+public:
+    BOM_CFG_PARSER( std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS>* aPluginList,
+                    const char* aData, const wxString& aSource );
+
+    void Parse();
+
+private:
+    void parseGenerator();
+};
+
+
+std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS> EESCHEMA_SETTINGS::DefaultBomPlugins()
+{
+    return bomSettingsFromJson( defaultBomPlugins );
+}
+
+
+bool EESCHEMA_SETTINGS::migrateBomSettings()
+{
+    nlohmann::json::json_pointer ptr = PointerFromString( "bom.plugins" );
+
+    if( !contains( ptr ) )
+        return false;
+
+    wxString list = at( ptr ).get<wxString>();
+
+    BOM_CFG_PARSER cfg_parser( &m_BomPanel.plugins, TO_UTF8( list ), wxT( "plugins" ) );
+
+    try
+    {
+        cfg_parser.Parse();
+    }
+    catch( const IO_ERROR& )
+    {
+        return false;
+    }
+
+    // Parser will have loaded up our array, let's dump it out to JSON
+    at( ptr ) = bomSettingsToJson();
+
+    return true;
+}
+
+
+nlohmann::json EESCHEMA_SETTINGS::bomSettingsToJson() const
+{
+    nlohmann::json js = nlohmann::json::array();
+
+    for( const BOM_PLUGIN_SETTINGS& plugin : m_BomPanel.plugins )
+    {
+        nlohmann::json pluginJson;
+
+        pluginJson["name"]    = plugin.name.ToUTF8();
+        pluginJson["path"]    = plugin.path.ToUTF8();
+        pluginJson["command"] = plugin.command.ToUTF8();
+
+        js.push_back( pluginJson );
+    }
+
+    return js;
+}
+
+
+std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS> EESCHEMA_SETTINGS::bomSettingsFromJson(
+        const nlohmann::json& aObj )
+{
+    std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS> ret;
+
+    wxASSERT( aObj.is_array() );
+
+    for( const nlohmann::json& entry : aObj )
+    {
+        if( entry.empty() || !entry.is_object() )
+            continue;
+
+        if( !entry.contains( "name" ) || !entry.contains( "path" ) )
+            continue;
+
+        BOM_PLUGIN_SETTINGS plugin( entry.at( "name" ).get<wxString>(),
+                                    entry.at( "path" ).get<wxString>() );
+
+        if( entry.contains( "command" ) )
+            plugin.command = entry.at( "command" ).get<wxString>();
+
+        ret.emplace_back( plugin );
+    }
+
+    return ret;
+}
+
+
+BOM_CFG_PARSER::BOM_CFG_PARSER( std::vector<EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS>* aPluginList,
+                                const char* aLine, const wxString& aSource ) :
+        DIALOG_BOM_CFG_LEXER( aLine, aSource )
+{
+    wxASSERT( aPluginList );
+    m_pluginList = aPluginList;
+}
+
+
+void BOM_CFG_PARSER::Parse()
+{
+    T token;
+
+    while( ( token = NextTok() ) != T_RIGHT )
+    {
+        if( token == T_EOF)
+            break;
+
+        if( token == T_LEFT )
+            token = NextTok();
+
+        if( token == T_plugins )
+            continue;
+
+        switch( token )
+        {
+        case T_plugin:   // Defines a new plugin
+            parseGenerator();
+            break;
+
+        default:
+//            Unexpected( CurText() );
+            break;
+        }
+    }
+}
+
+
+void BOM_CFG_PARSER::parseGenerator()
+{
+    wxString str;
+    EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS settings;
+
+    NeedSYMBOLorNUMBER();
+    settings.path = FromUTF8();
+
+    T token;
+
+    while( ( token = NextTok() ) != T_RIGHT )
+    {
+        if( token == T_EOF)
+            break;
+
+        switch( token )
+        {
+        case T_LEFT:
+            break;
+
+        case T_cmd:
+            NeedSYMBOLorNUMBER();
+
+            settings.command = FromUTF8();
+
+            NeedRIGHT();
+            break;
+
+        case T_opts:
+        {
+            NeedSYMBOLorNUMBER();
+
+            wxString option = FromUTF8();
+
+            if( option.StartsWith( "nickname=", &str ) )
+                settings.name = str;
+
+            NeedRIGHT();
+            break;
+        }
+
+        default:
+            Unexpected( CurText() );
+            break;
+        }
+    }
+
+    m_pluginList->emplace_back( settings );
+}
diff --git a/eeschema/eeschema_settings.h b/eeschema/eeschema_settings.h
index e837617d51..a30db095ef 100644
--- a/eeschema/eeschema_settings.h
+++ b/eeschema/eeschema_settings.h
@@ -57,6 +57,20 @@ public:
         bool align_to_grid;
     };
 
+    struct BOM_PLUGIN_SETTINGS
+    {
+        BOM_PLUGIN_SETTINGS() = default;
+
+        BOM_PLUGIN_SETTINGS( const wxString& aName, const wxString& aPath ) :
+                name( aName ),
+                path( aPath )
+        {}
+
+        wxString name;
+        wxString path;
+        wxString command;
+    };
+
     struct DRAWING
     {
         int      default_bus_thickness;
@@ -126,7 +140,7 @@ public:
     struct PANEL_BOM
     {
         wxString selected_plugin;
-        wxString plugins;
+        std::vector<BOM_PLUGIN_SETTINGS> plugins;
     };
 
     struct PANEL_FIELD_EDITOR
@@ -188,6 +202,8 @@ public:
 
     virtual bool MigrateFromLegacy( wxConfigBase* aLegacyConfig ) override;
 
+    static std::vector<BOM_PLUGIN_SETTINGS> DefaultBomPlugins();
+
     APPEARANCE m_Appearance;
 
     AUTOPLACE_FIELDS m_AutoplaceFields;
@@ -223,6 +239,14 @@ public:
 protected:
 
     virtual std::string getLegacyFrameName() const override { return "SchematicFrame"; }
+
+private:
+
+    bool migrateBomSettings();
+
+    nlohmann::json bomSettingsToJson() const;
+
+    static std::vector<BOM_PLUGIN_SETTINGS> bomSettingsFromJson( const nlohmann::json& aObj );
 };