diff --git a/webob/response.py b/webob/response.py
index a164938..9579b7e 100644
--- a/webob/response.py
+++ b/webob/response.py
@@ -116,16 +116,13 @@ class Response(object):
         if 'charset' in kw:
             charset = kw.pop('charset')
         elif self.default_charset:
-            if (content_type
-                and 'charset=' not in content_type
-                and (content_type == 'text/html'
-                    or content_type.startswith('text/')
-                    or content_type.startswith('application/xml')
-                    or content_type.startswith('application/json')
-                    or (content_type.startswith('application/')
-                         and (content_type.endswith('+xml') or content_type.endswith('+json'))))):
-                charset = self.default_charset
-        if content_type and charset:
+            if content_type and 'charset=' not in content_type:
+                if (content_type == 'text/html'
+                        or content_type.startswith('text/')
+                        or _is_xml(content_type)
+                        or _is_json(content_type)):
+                    charset = self.default_charset
+        if content_type and charset and not _is_json(content_type):
             content_type += '; charset=' + charset
         elif self._headerlist and charset:
             self.charset = charset
@@ -1231,6 +1228,16 @@ class EmptyResponse(object):
 
     __next__ = next # py3
 
+def _is_json(content_type):
+    return (content_type.startswith('application/json')
+            or (content_type.startswith('application/')
+                and content_type.endswith('+json')))
+
+def _is_xml(content_type):
+    return (content_type.startswith('application/xml')
+            or (content_type.startswith('application/')
+                and content_type.endswith('+xml')))
+
 def _request_uri(environ):
     """Like wsgiref.url.request_uri, except eliminates :80 ports
 

JSON content shouldn't need a UTF-8 on the content-type
Fix this issue: https://github.com/Pylons/pyramid/issues/1611#issuecomment-93073442
diff --git a/tests/test_response.py b/tests/test_response.py
index d9c1cd3..6425a22 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -104,6 +104,7 @@ def test_set_response_status_code_generic_reason():
     assert res.status_code == 299
     assert res.status == '299 Success'
 
+
 def test_content_type():
     r = Response()
     # default ctype and charset
@@ -121,6 +122,21 @@ def test_init_content_type_w_charset():
     v = 'text/plain;charset=ISO-8859-1'
     eq_(Response(content_type=v).headers['content-type'], v)
 
+def test_init_adds_default_charset_when_not_json():
+    content_type = 'text/plain'
+    expected = 'text/plain; charset=UTF-8'
+    eq_(Response(content_type=content_type).headers['content-type'], expected)
+
+def test_init_no_charset_when_json():
+    content_type = 'application/json'
+    expected = content_type
+    eq_(Response(content_type=content_type).headers['content-type'], expected)
+
+def test_init_keeps_specified_charset_when_json():
+    content_type = 'application/json;charset=ISO-8859-1'
+    expected = content_type
+    eq_(Response(content_type=content_type).headers['content-type'], expected)
+
 
 def test_cookies():
     res = Response()

pip install -e .[dev]
parse_log_pytest
pytest
nose coverage pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
nose==1.3.7
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/Pylons/webob.git@9b79f5f913fb1f07c68102a2279ed757a2a9abf6#egg=WebOb

name: webob
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - nose==1.3.7
prefix: /opt/conda/envs/webob

tests/test_response.py::test_init_no_charset_when_json
tests/test_response.py::test_response tests/test_response.py::test_set_response_status_binary tests/test_response.py::test_set_response_status_str_no_reason tests/test_response.py::test_set_response_status_str_generic_reason tests/test_response.py::test_set_response_status_code tests/test_response.py::test_set_response_status_bad tests/test_response.py::test_set_response_status_code_generic_reason tests/test_response.py::test_content_type tests/test_response.py::test_init_content_type_w_charset tests/test_response.py::test_init_adds_default_charset_when_not_json tests/test_response.py::test_init_keeps_specified_charset_when_json tests/test_response.py::test_cookies tests/test_response.py::test_unicode_cookies_error_raised tests/test_response.py::test_unicode_cookies_warning_issued tests/test_response.py::test_http_only_cookie tests/test_response.py::test_headers tests/test_response.py::test_response_copy tests/test_response.py::test_response_copy_content_md5 tests/test_response.py::test_HEAD_closes tests/test_response.py::test_HEAD_conditional_response_returns_empty_response tests/test_response.py::test_HEAD_conditional_response_range_empty_response tests/test_response.py::test_conditional_response_if_none_match_false tests/test_response.py::test_conditional_response_if_none_match_true tests/test_response.py::test_conditional_response_if_none_match_weak tests/test_response.py::test_conditional_response_if_modified_since_false tests/test_response.py::test_conditional_response_if_modified_since_true tests/test_response.py::test_conditional_response_range_not_satisfiable_response tests/test_response.py::test_HEAD_conditional_response_range_not_satisfiable_response tests/test_response.py::test_md5_etag tests/test_response.py::test_md5_etag_set_content_md5 tests/test_response.py::test_decode_content_defaults_to_identity tests/test_response.py::test_decode_content_with_deflate tests/test_response.py::test_content_length tests/test_response.py::test_app_iter_range tests/test_response.py::test_app_iter_range_inner_method tests/test_response.py::test_content_type_in_headerlist tests/test_response.py::test_str_crlf tests/test_response.py::test_from_file tests/test_response.py::test_from_file2 tests/test_response.py::test_from_text_file tests/test_response.py::test_from_file_w_leading_space_in_header tests/test_response.py::test_file_bad_header tests/test_response.py::test_from_file_not_unicode_headers tests/test_response.py::test_set_status tests/test_response.py::test_set_headerlist tests/test_response.py::test_request_uri_no_script_name tests/test_response.py::test_request_uri_https tests/test_response.py::test_app_iter_range_starts_after_iter_end tests/test_response.py::test_resp_write_app_iter_non_list tests/test_response.py::test_response_file_body_writelines tests/test_response.py::test_response_write_non_str tests/test_response.py::test_response_file_body_write_empty_app_iter tests/test_response.py::test_response_file_body_write_empty_body tests/test_response.py::test_response_file_body_close_not_implemented tests/test_response.py::test_response_file_body_repr tests/test_response.py::test_body_get_is_none tests/test_response.py::test_body_get_is_unicode_notverylong tests/test_response.py::test_body_get_is_unicode tests/test_response.py::test_body_set_not_unicode_or_str tests/test_response.py::test_body_set_unicode tests/test_response.py::test_body_set_under_body_doesnt_exist tests/test_response.py::test_body_del tests/test_response.py::test_text_get_no_charset tests/test_response.py::test_unicode_body tests/test_response.py::test_text_get_decode tests/test_response.py::test_text_set_no_charset tests/test_response.py::test_text_set_not_unicode tests/test_response.py::test_text_del tests/test_response.py::test_body_file_del tests/test_response.py::test_write_unicode tests/test_response.py::test_write_unicode_no_charset tests/test_response.py::test_write_text tests/test_response.py::test_app_iter_del tests/test_response.py::test_charset_set_no_content_type_header tests/test_response.py::test_charset_del_no_content_type_header tests/test_response.py::test_content_type_params_get_no_semicolon_in_content_type_header tests/test_response.py::test_content_type_params_get_semicolon_in_content_type_header tests/test_response.py::test_content_type_params_set_value_dict_empty tests/test_response.py::test_content_type_params_set_ok_param_quoting tests/test_response.py::test_set_cookie_overwrite tests/test_response.py::test_set_cookie_value_is_None tests/test_response.py::test_set_cookie_expires_is_None_and_max_age_is_int tests/test_response.py::test_set_cookie_expires_is_None_and_max_age_is_timedelta tests/test_response.py::test_set_cookie_expires_is_not_None_and_max_age_is_None tests/test_response.py::test_set_cookie_expires_is_timedelta_and_max_age_is_None tests/test_response.py::test_delete_cookie tests/test_response.py::test_delete_cookie_with_path tests/test_response.py::test_delete_cookie_with_domain tests/test_response.py::test_unset_cookie_not_existing_and_not_strict tests/test_response.py::test_unset_cookie_not_existing_and_strict tests/test_response.py::test_unset_cookie_key_in_cookies tests/test_response.py::test_merge_cookies_no_set_cookie tests/test_response.py::test_merge_cookies_resp_is_Response tests/test_response.py::test_merge_cookies_resp_is_wsgi_callable tests/test_response.py::test_body_get_body_is_None_len_app_iter_is_zero tests/test_response.py::test_cache_control_get tests/test_response.py::test_location tests/test_response.py::test_request_uri_http tests/test_response.py::test_request_uri_no_script_name2 tests/test_response.py::test_cache_control_object_max_age_ten tests/test_response.py::test_cache_control_set_object_error tests/test_response.py::test_cache_expires_set tests/test_response.py::test_status_code_set tests/test_response.py::test_cache_control_set_dict tests/test_response.py::test_cache_control_set_None tests/test_response.py::test_cache_control_set_unicode tests/test_response.py::test_cache_control_set_control_obj_is_not_None tests/test_response.py::test_cache_control_del tests/test_response.py::test_body_file_get tests/test_response.py::test_body_file_write_no_charset tests/test_response.py::test_body_file_write_unicode_encodes tests/test_response.py::test_repr tests/test_response.py::test_cache_expires_set_timedelta tests/test_response.py::test_cache_expires_set_int tests/test_response.py::test_cache_expires_set_None tests/test_response.py::test_cache_expires_set_zero tests/test_response.py::test_encode_content_unknown tests/test_response.py::test_encode_content_identity tests/test_response.py::test_encode_content_gzip_already_gzipped tests/test_response.py::test_encode_content_gzip_notyet_gzipped tests/test_response.py::test_encode_content_gzip_notyet_gzipped_lazy tests/test_response.py::test_encode_content_gzip_buffer_coverage tests/test_response.py::test_decode_content_identity tests/test_response.py::test_decode_content_weird tests/test_response.py::test_decode_content_gzip tests/test_response.py::test__abs_headerlist_location_with_scheme tests/test_response.py::test__abs_headerlist_location_no_scheme tests/test_response.py::test_response_set_body_file1 tests/test_response.py::test_response_set_body_file2 tests/test_response.py::test_response_json_body tests/test_response.py::test_cache_expires_set_zero_then_nonzero
webob/response.py
webob/response.py