diff --git a/app/models/integration.rb b/app/models/integration.rb
index 13ef37e015778cb838a08b568d76dad007accf1b..c905788ac8bfd59688bceb01b834d12b8999e60c 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -161,7 +161,7 @@ def self.fields
   end
 
   def fields
-    self.class.fields
+    self.class.fields.dup
   end
 
   # Provide convenient accessor methods for each serialized property.
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index b384a94d713737eb1156286c0b8413f5e0600edd..4e144a688f6e90760028f6b95aae81b0c7c5c794 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -5,7 +5,26 @@ class Bamboo < BaseCi
     include ReactivelyCached
     prepend EnableSslVerification
 
-    prop_accessor :bamboo_url, :build_key, :username, :password
+    field :bamboo_url,
+      title: s_('BambooService|Bamboo URL'),
+      placeholder: s_('https://bamboo.example.com'),
+      help: s_('BambooService|Bamboo service root URL.'),
+      required: true
+
+    field :build_key,
+      help: s_('BambooService|Bamboo build plan key.'),
+      non_empty_password_title: s_('BambooService|Enter new build key'),
+      non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
+      placeholder: s_('KEY'),
+      required: true
+
+    field :username,
+      help: s_('BambooService|The user with API access to the Bamboo server.')
+
+    field :password,
+      type: 'password',
+      non_empty_password_title: s_('ProjectService|Enter new password'),
+      non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
 
     validates :bamboo_url, presence: true, public_url: true, if: :activated?
     validates :build_key, presence: true, if: :activated?
@@ -43,39 +62,6 @@ def self.to_param
       'bamboo'
     end
 
-    def fields
-      [
-          {
-            type: 'text',
-            name: 'bamboo_url',
-            title: s_('BambooService|Bamboo URL'),
-            placeholder: s_('https://bamboo.example.com'),
-            help: s_('BambooService|Bamboo service root URL.'),
-            required: true
-          },
-          {
-            type: 'password',
-            name: 'build_key',
-            help: s_('BambooService|Bamboo build plan key.'),
-            non_empty_password_title: s_('BambooService|Enter new build key'),
-            non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
-            placeholder: s_('KEY'),
-            required: true
-          },
-          {
-            type: 'text',
-            name: 'username',
-            help: s_('BambooService|The user with API access to the Bamboo server.')
-          },
-          {
-            type: 'password',
-            name: 'password',
-            non_empty_password_title: s_('ProjectService|Enter new password'),
-            non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
-          }
-      ]
-    end
-
     def build_page(sha, ref)
       with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
     end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 3b802271a36f2b49ed688e7d068bf277acdfba97..d1e54ce86ee97d544c358cafbf98da034f509376 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -10,7 +10,18 @@ class Buildkite < BaseCi
 
     ENDPOINT = "https://buildkite.com"
 
-    prop_accessor :project_url, :token
+    field :project_url,
+      title: _('Pipeline URL'),
+      placeholder: "#{ENDPOINT}/example-org/test-pipeline",
+      required: true
+
+    field :token,
+      type: 'password',
+      title: _('Token'),
+      help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
+      non_empty_password_title: s_('ProjectService|Enter new token'),
+      non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+      required: true
 
     validates :project_url, presence: true, public_url: true, if: :activated?
     validates :token, presence: true, if: :activated?
@@ -74,24 +85,6 @@ def help
       s_('ProjectService|Run CI/CD pipelines with Buildkite.')
     end
 
-    def fields
-      [
-        { type: 'password',
-          name: 'token',
-          title: _('Token'),
-          help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
-          non_empty_password_title: s_('ProjectService|Enter new token'),
-          non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
-          required: true },
-
-        { type: 'text',
-          name: 'project_url',
-          title: _('Pipeline URL'),
-          placeholder: "#{ENDPOINT}/example-org/test-pipeline",
-          required: true }
-      ]
-    end
-
     def calculate_reactive_cache(sha, ref)
       response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
 
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index 73f78bec38165bb0b6f17c044f1ed6ac0c50b5b5..0c65ed8cd5fab0eb98b0384a3e27e68ec471528b 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -10,7 +10,17 @@ class DroneCi < BaseCi
 
     DRONE_SAAS_HOSTNAME = 'cloud.drone.io'
 
-    prop_accessor :drone_url, :token
+    field :drone_url,
+      title: s_('ProjectService|Drone server URL'),
+      placeholder: 'http://drone.example.com',
+      required: true
+
+    field :token,
+      type: 'password',
+      help: s_('ProjectService|Token for the Drone project.'),
+      non_empty_password_title: s_('ProjectService|Enter new token'),
+      non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
+      required: true
 
     validates :drone_url, presence: true, public_url: true, if: :activated?
     validates :token, presence: true, if: :activated?
@@ -94,26 +104,6 @@ def help
       s_('ProjectService|Run CI/CD pipelines with Drone.')
     end
 
-    def fields
-      [
-        {
-          type: 'password',
-          name: 'token',
-          help: s_('ProjectService|Token for the Drone project.'),
-          non_empty_password_title: s_('ProjectService|Enter new token'),
-          non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
-          required: true
-        },
-        {
-          type: 'text',
-          name: 'drone_url',
-          title: s_('ProjectService|Drone server URL'),
-          placeholder: 'http://drone.example.com',
-          required: true
-        }
-      ]
-    end
-
     override :hook_url
     def hook_url
       [drone_url, "/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 83838ac1b5376366e385c1079d87ae0a77a3cf18..a1abbce72bc32ead4cb36f103417391e331867f2 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -7,7 +7,25 @@ class Jenkins < BaseCi
     prepend EnableSslVerification
     extend Gitlab::Utils::Override
 
-    prop_accessor :jenkins_url, :project_name, :username, :password
+    field :jenkins_url,
+      title: s_('ProjectService|Jenkins server URL'),
+      required: true,
+      placeholder: 'http://jenkins.example.com',
+      help: s_('The URL of the Jenkins server.')
+
+    field :project_name,
+      required: true,
+      placeholder: 'my_project_name',
+      help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
+
+    field :username,
+      help: s_('The username for the Jenkins server.')
+
+    field :password,
+      type: 'password',
+      help: s_('The password for the Jenkins server.'),
+      non_empty_password_title: s_('ProjectService|Enter new password.'),
+      non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
 
     before_validation :reset_password
 
@@ -71,37 +89,5 @@ def help
     def self.to_param
       'jenkins'
     end
-
-    def fields
-      [
-        {
-          type: 'text',
-          name: 'jenkins_url',
-          title: s_('ProjectService|Jenkins server URL'),
-          required: true,
-          placeholder: 'http://jenkins.example.com',
-          help: s_('The URL of the Jenkins server.')
-        },
-        {
-          type: 'text',
-          name: 'project_name',
-          required: true,
-          placeholder: 'my_project_name',
-          help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
-        },
-        {
-          type: 'text',
-          name: 'username',
-          help: s_('The username for the Jenkins server.')
-        },
-        {
-          type: 'password',
-          name: 'password',
-          help: s_('The password for the Jenkins server.'),
-          non_empty_password_title: s_('ProjectService|Enter new password.'),
-          non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
-        }
-      ]
-    end
   end
 end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 568fb609a44d4da8fb5d6080f614531b8e56c486..cd2928136efb341f2ff7c84a4a7c2e6d35cf02d4 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -7,7 +7,11 @@ class MockCi < BaseCi
 
     ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
 
-    prop_accessor :mock_service_url
+    field :mock_service_url,
+      title: s_('ProjectService|Mock service URL'),
+      placeholder: 'http://localhost:4004',
+      required: true
+
     validates :mock_service_url, presence: true, public_url: true, if: :activated?
 
     def title
@@ -22,18 +26,6 @@ def self.to_param
       'mock_ci'
     end
 
-    def fields
-      [
-        {
-          type: 'text',
-          name: 'mock_service_url',
-          title: s_('ProjectService|Mock service URL'),
-          placeholder: 'http://localhost:4004',
-          required: true
-        }
-      ]
-    end
-
     # Return complete url to build page
     #
     # Ex.
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index f0f83f118d7e4feffc557c0ee5a5c7fd8d8247cb..1205173e40b5a9e4c78e28d3db083d937fd2d12a 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -8,7 +8,22 @@ class Teamcity < BaseCi
 
     TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze
 
-    prop_accessor :teamcity_url, :build_type, :username, :password
+    field :teamcity_url,
+      title: s_('ProjectService|TeamCity server URL'),
+      placeholder: 'https://teamcity.example.com',
+      required: true
+
+    field :build_type,
+      help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
+      required: true
+
+    field :username,
+      help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
+
+    field :password,
+      type: 'password',
+      non_empty_password_title: s_('ProjectService|Enter new password'),
+      non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
 
     validates :teamcity_url, presence: true, public_url: true, if: :activated?
     validates :build_type, presence: true, if: :activated?
@@ -51,35 +66,6 @@ def help
       s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
     end
 
-    def fields
-      [
-        {
-          type: 'text',
-          name: 'teamcity_url',
-          title: s_('ProjectService|TeamCity server URL'),
-          placeholder: 'https://teamcity.example.com',
-          required: true
-        },
-        {
-          type: 'text',
-          name: 'build_type',
-          help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
-          required: true
-        },
-        {
-          type: 'text',
-          name: 'username',
-          help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
-        },
-        {
-          type: 'password',
-          name: 'password',
-          non_empty_password_title: s_('ProjectService|Enter new password'),
-          non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
-        }
-      ]
-    end
-
     def build_page(sha, ref)
       with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
     end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index a2b914c42f22f8fd366c0797599702075bad46d1..7b524be0c43a7564293647d57a748c635fa62838 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -782,8 +782,16 @@
     end
   end
 
-  describe '#api_field_names' do
-    shared_examples 'api field names' do
+  describe 'field definitions' do
+    shared_examples '#fields' do
+      it 'does not return the same array' do
+        integration = fake_integration.new
+
+        expect(integration.fields).not_to be(integration.fields)
+      end
+    end
+
+    shared_examples '#api_field_names' do
       it 'filters out secret fields' do
         safe_fields = %w[some_safe_field safe_field url trojan_gift]
 
@@ -816,7 +824,8 @@ def fields
         end
       end
 
-      it_behaves_like 'api field names'
+      it_behaves_like '#fields'
+      it_behaves_like '#api_field_names'
     end
 
     context 'when the class uses the field DSL' do
@@ -839,7 +848,8 @@ def fields
         end
       end
 
-      it_behaves_like 'api field names'
+      it_behaves_like '#fields'
+      it_behaves_like '#api_field_names'
     end
   end
 
diff --git a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
index c698e06c2a296274724e88327bca72038db4d187..fbec6f98e7613d5fa04adfe3dec8f33ae2a626de 100644
--- a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
+++ b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
@@ -43,5 +43,9 @@
 
       expect(names.index('enable_ssl_verification')).to eq insert_index
     end
+
+    it 'does not insert the field repeatedly' do
+      expect(integration.fields.pluck(:name)).to eq(integration.fields.pluck(:name))
+    end
   end
 end