20'000 objects isn't very much. 
You should be able to do it in less than five seconds 700ms.
Your way is very slow because you do 
foreach(jsonObject)
   connection.open()
   cmd.execute() // Individual insert-statement
   connection.close()
Opening and Closing a connection is expensive, as is executing each command separately.
You should instead do
int batchSize = 100;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
connection.Open();
transaction.Begin();
for(int i = 0; i < Count;++i)
{ 
     sb.Append("SQL-Insert");
     if(i%batchSize == 0 && i != 0)
     {
          execute(sb.ToString())
          sb.Length = 0;
     }
}
execute(sb.ToString())
transaction.commit();
// TODO: Try/Catch + Rollback
connection.Close();
A much easier way is to fetch the table you want to insert your objects to with SELECT * FROM table_name WHERE (1=2)
then add the entries to the datatable, and then call DataAdaper.Update()
If your table has a primary-key, you can even auto-generate the insert command using SqlCommandBuilder.
C#
public static void InsertUpdateDataTable(string strTableName, System.Data.DataTable dt)
{
    string strSQL = string.Format("SELECT * FROM [{0}] WHERE 1 = 2 ", strTableName.Replace("]", "]]"));
    using (System.Data.SqlClient.SqlDataAdapter daInsertUpdate = new System.Data.SqlClient.SqlDataAdapter(strSQL, getConnectionString())) {
        SqlCommandBuilder cmdBuilder = new SqlCommandBuilder(daInsertUpdate);
        daInsertUpdate.InsertCommand = cmdBuilder.GetInsertCommand();
        daInsertUpdate.UpdateCommand = cmdBuilder.GetUpdateCommand();
        daInsertUpdate.Update(dt);
    }
}
VB.NET:
Public Shared Sub InsertUpdateDataTable(strTableName As String, dt As System.Data.DataTable)
    Dim strSQL As String = String.Format("SELECT * FROM [{0}] WHERE 1 = 2 ", strTableName.Replace("]", "]]"))
    Using daInsertUpdate As New System.Data.SqlClient.SqlDataAdapter(strSQL, getConnectionString())
        Dim cmdBuilder As New SqlCommandBuilder(daInsertUpdate)
        daInsertUpdate.InsertCommand = cmdBuilder.GetInsertCommand()
        daInsertUpdate.UpdateCommand = cmdBuilder.GetUpdateCommand()
        daInsertUpdate.Update(dt)
    End Using
End Sub
You can set the batch-size on the DataAdaper, and it generates the SQL for you as well.
And in a database-agnostic way:
private static System.Data.Common.DbProviderFactory m_factory = System.Data.Common.DbProviderFactories.GetFactory(typeof(System.Data.SqlClient.SqlClientFactory).Namespace);
public static void InsertUpdateDataTable(string strTableName, System.Data.DataTable dt)
{
    if (dt == null)
        throw new System.ArgumentNullException("DataTable dt may not be NULL.");
    // https://msdn.microsoft.com/en-us/library/aadf8fk2(v=vs.110).aspx
    using (System.Data.Common.DbDataAdapter daInsertUpdate = m_factory.CreateDataAdapter())
    {
        using (System.Data.Common.DbConnection conn = m_factory.CreateConnection())
        {
            conn.ConnectionString = getConnectionString();
            daInsertUpdate.SelectCommand = conn.CreateCommand();
            daInsertUpdate.SelectCommand.CommandText = string.Format("SELECT * FROM [{0}] WHERE 1 = 2 ", strTableName.Replace("]", "]]"));
            using (System.Data.Common.DbCommandBuilder cmdBuilder = m_factory.CreateCommandBuilder())
            {
                cmdBuilder.DataAdapter = daInsertUpdate;
                daInsertUpdate.InsertCommand = cmdBuilder.GetInsertCommand();
                daInsertUpdate.UpdateCommand = cmdBuilder.GetUpdateCommand();
            } // End Using cmdBuilder
            daInsertUpdate.Update(dt);
        } // End Using conn
    } // End Using daInsertUpdate 
    System.Console.WriteLine(dt);
}
And using a transaction (assuming ls is a list of numbers):
private static System.Data.Common.DbProviderFactory m_factory = System.Data.Common.DbProviderFactories.GetFactory(typeof(System.Data.SqlClient.SqlClientFactory).Namespace);
public static string getConnectionString()
{
    System.Data.SqlClient.SqlConnectionStringBuilder csb = new System.Data.SqlClient.SqlConnectionStringBuilder();
    csb.DataSource = System.Environment.MachineName;
    csb.InitialCatalog = "TestDb";
    csb.IntegratedSecurity = true;
    return csb.ConnectionString;
}
public static System.Data.Common.DbConnection GetConnection()
{
    var con = m_factory.CreateConnection();
    con.ConnectionString = getConnectionString();
    return con;
}
public static int BatchedInsert(System.Collections.IList ls)
{
    int iAffected = 0;
    int batchSize = 100; // Each batch corresponds to a single round-trip to the DB.
    using (System.Data.IDbConnection idbConn = GetConnection())
    {
        lock (idbConn)
        {
            using (System.Data.IDbCommand cmd = idbConn.CreateCommand())
            {
                lock (cmd)
                {
                    if (cmd.Connection.State != System.Data.ConnectionState.Open)
                        cmd.Connection.Open();
                    using (System.Data.IDbTransaction idbtTrans = idbConn.BeginTransaction())
                    {
                        try
                        {
                            cmd.Transaction = idbtTrans;
                            System.Text.StringBuilder sb = new System.Text.StringBuilder();
                            for (int i = 0; i < ls.Count; ++i)
                            {
                                sb.Append("INSERT INTO T_TransactionInsertTest(TestValue) VALUES ( ");
                                sb.Append(ls[i].ToString());
                                sb.AppendLine(");");
                                if (i % batchSize == 0 && i != 0)
                                {
                                    cmd.CommandText = sb.ToString();
                                    iAffected += cmd.ExecuteNonQuery();
                                    sb.Length = 0;
                                }
                            }
                            if (sb.Length != 0)
                            {
                                cmd.CommandText = sb.ToString();
                                iAffected += cmd.ExecuteNonQuery();
                            }
                            idbtTrans.Commit();
                        } // End Try
                        catch (System.Data.Common.DbException ex)
                        {
                            if (idbtTrans != null)
                                idbtTrans.Rollback();
                            iAffected = -1;
                            //if (Log(ex))
                            throw;
                        } // End catch
                        finally
                        {
                            if (cmd.Connection.State != System.Data.ConnectionState.Closed)
                                cmd.Connection.Close();
                        } // End Finally
                    } // End Using idbtTrans
                } // End lock cmd
            } // End Using cmd 
        } // End lock idbConn
    } // End Using idbConn
    return iAffected;
} // End Function BatchedInsert