Repository: httprunner/httprunner (language: python)

Issue:
convert postman collection to HttpRunner testcase



Discussion / hints:
@binbinu 还没支持这部分转换功能

Fix patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 2d00f359d..a50a37a98 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -9,6 +9,8 @@
 - fix #1308: load `.env` file as environment variables
 - fix #1309: locate plugin file upward recursively until system root dir
 - refactor: move base_url to config env
+- feat: support converting Postman collection to HttpRunner testcase
+- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter`
 
 ## v4.1.0-beta (2022-05-21)
 
diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md
index 5e74e8392..74a6c8b2b 100644
--- a/docs/cmd/hrp.md
+++ b/docs/cmd/hrp.md
@@ -30,7 +30,7 @@ Copyright 2017 debugtalk
 ### SEE ALSO
 
 * [hrp boom](hrp_boom.md)	 - run load test with boomer
-* [hrp convert](hrp_convert.md)	 - convert JSON/YAML testcases to pytest/gotest scripts
+* [hrp convert](hrp_convert.md)	 - convert to JSON/YAML/gotest/pytest testcases
 * [hrp har2case](hrp_har2case.md)	 - convert HAR to json/yaml testcase files
 * [hrp pytest](hrp_pytest.md)	 - run API test with pytest
 * [hrp run](hrp_run.md)	 - run API test with go engine
diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md
index d778b691d..c3b1b5cce 100644
--- a/docs/cmd/hrp_convert.md
+++ b/docs/cmd/hrp_convert.md
@@ -1,6 +1,6 @@
 ## hrp convert
 
-convert JSON/YAML testcases to pytest/gotest scripts
+convert to JSON/YAML/gotest/pytest testcases
 
 ```
 hrp convert $path... [flags]
@@ -9,9 +9,13 @@ hrp convert $path... [flags]
 ### Options
 
 ```
-      --gotest   convert to gotest scripts (TODO)
-  -h, --help     help for convert
-      --pytest   convert to pytest scripts (default true)
+  -h, --help                help for convert
+  -d, --output-dir string   specify output directory, default to the same dir with har file
+  -p, --profile string      specify profile path to override headers (except for auto-generated headers) and cookies
+      --to-gotest           convert to gotest scripts (TODO)
+      --to-json             convert to JSON scripts (default)
+      --to-pytest           convert to pytest scripts
+      --to-yaml             convert to YAML scripts
 ```
 
 ### SEE ALSO
diff --git a/examples/data/har/demo.json b/examples/data/har/demo.json
deleted file mode 100644
index 292ad5138..000000000
--- a/examples/data/har/demo.json
+++ /dev/null
@@ -1,128 +0,0 @@
-{
-    "config": {
-        "name": "testcase description"
-    },
-    "teststeps": [
-        {
-            "name": "",
-            "request": {
-                "method": "GET",
-                "url": "https://postman-echo.com/get",
-                "params": {
-                    "foo1": "HDnY8",
-                    "foo2": "34.5"
-                },
-                "headers": {
-                    "Accept-Encoding": "gzip",
-                    "Host": "postman-echo.com",
-                    "User-Agent": "HttpRunnerPlus"
-                }
-            },
-            "validate": [
-                {
-                    "check": "status_code",
-                    "assert": "equals",
-                    "expect": 200,
-                    "msg": "assert response status code"
-                },
-                {
-                    "check": "headers.\"Content-Type\"",
-                    "assert": "equals",
-                    "expect": "application/json; charset=utf-8",
-                    "msg": "assert response header Content-Type"
-                },
-                {
-                    "check": "body.url",
-                    "assert": "equals",
-                    "expect": "https://postman-echo.com/get?foo1=HDnY8\u0026foo2=34.5",
-                    "msg": "assert response body url"
-                }
-            ]
-        },
-        {
-            "name": "",
-            "request": {
-                "method": "POST",
-                "url": "https://postman-echo.com/post",
-                "headers": {
-                    "Accept-Encoding": "gzip",
-                    "Content-Length": "28",
-                    "Content-Type": "application/json; charset=UTF-8",
-                    "Host": "postman-echo.com",
-                    "User-Agent": "Go-http-client/1.1"
-                },
-                "cookies": {
-                    "sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
-                },
-                "body": {
-                    "foo1": "HDnY8",
-                    "foo2": 12.3
-                }
-            },
-            "validate": [
-                {
-                    "check": "status_code",
-                    "assert": "equals",
-                    "expect": 200,
-                    "msg": "assert response status code"
-                },
-                {
-                    "check": "headers.\"Content-Type\"",
-                    "assert": "equals",
-                    "expect": "application/json; charset=utf-8",
-                    "msg": "assert response header Content-Type"
-                },
-                {
-                    "check": "body.url",
-                    "assert": "equals",
-                    "expect": "https://postman-echo.com/post",
-                    "msg": "assert response body url"
-                }
-            ]
-        },
-        {
-            "name": "",
-            "request": {
-                "method": "POST",
-                "url": "https://postman-echo.com/post",
-                "headers": {
-                    "Accept-Encoding": "gzip",
-                    "Content-Length": "20",
-                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
-                    "Host": "postman-echo.com",
-                    "User-Agent": "Go-http-client/1.1"
-                },
-                "cookies": {
-                    "sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
-                },
-                "body": "foo1=HDnY8\u0026foo2=12.3"
-            },
-            "validate": [
-                {
-                    "check": "status_code",
-                    "assert": "equals",
-                    "expect": 200,
-                    "msg": "assert response status code"
-                },
-                {
-                    "check": "headers.\"Content-Type\"",
-                    "assert": "equals",
-                    "expect": "application/json; charset=utf-8",
-                    "msg": "assert response header Content-Type"
-                },
-                {
-                    "check": "body.data",
-                    "assert": "equals",
-                    "expect": "",
-                    "msg": "assert response body data"
-                },
-                {
-                    "check": "body.url",
-                    "assert": "equals",
-                    "expect": "https://postman-echo.com/post",
-                    "msg": "assert response body url"
-                }
-            ]
-        }
-    ]
-}
\ No newline at end of file
diff --git a/examples/data/har/profile.yml b/examples/data/har/profile_override.yml
similarity index 86%
rename from examples/data/har/profile.yml
rename to examples/data/har/profile_override.yml
index 69963ba2e..35236a52f 100644
--- a/examples/data/har/profile.yml
+++ b/examples/data/har/profile_override.yml
@@ -1,3 +1,4 @@
+override: true
 headers:
     Content-Type: "application/x-www-form-urlencoded"
 cookies:
diff --git a/examples/data/postman/postman_collection.json b/examples/data/postman/postman_collection.json
new file mode 100644
index 000000000..3b7a9e307
--- /dev/null
+++ b/examples/data/postman/postman_collection.json
@@ -0,0 +1,488 @@
+{
+	"info": {
+		"_postman_id": "0417a445-b206-4ea2-b1d2-5441afd6c6b9",
+		"name": "postman collection demo",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+	},
+	"item": [
+		{
+			"name": "folder1",
+			"item": [
+				{
+					"name": "folder2",
+					"item": [
+						{
+							"name": "Get with params",
+							"request": {
+								"method": "GET",
+								"header": [],
+								"url": {
+									"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
+									"protocol": "https",
+									"host": [
+										"postman-echo",
+										"com"
+									],
+									"path": [
+										":path"
+									],
+									"query": [
+										{
+											"key": "k1",
+											"value": "v1"
+										},
+										{
+											"key": "k2",
+											"value": "v2"
+										},
+										{
+											"key": "k3",
+											"value": "v3",
+											"disabled": true
+										}
+									],
+									"variable": [
+										{
+											"key": "path",
+											"value": "get"
+										}
+									]
+								}
+							},
+							"response": [
+								{
+									"name": "Get with params case1",
+									"originalRequest": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
+											"protocol": "https",
+											"host": [
+												"postman-echo",
+												"com"
+											],
+											"path": [
+												":path"
+											],
+											"query": [
+												{
+													"key": "k1",
+													"value": "v1"
+												},
+												{
+													"key": "k2",
+													"value": "v2"
+												},
+												{
+													"key": "k3",
+													"value": "v3",
+													"disabled": true
+												}
+											],
+											"variable": [
+												{
+													"key": "path",
+													"value": "get"
+												}
+											]
+										}
+									},
+									"status": "OK",
+									"code": 200,
+									"_postman_previewlanguage": "json",
+									"header": [
+										{
+											"key": "Date",
+											"value": "Mon, 16 May 2022 12:12:28 GMT"
+										},
+										{
+											"key": "Content-Type",
+											"value": "application/json; charset=utf-8"
+										},
+										{
+											"key": "Content-Length",
+											"value": "508"
+										},
+										{
+											"key": "Connection",
+											"value": "keep-alive"
+										},
+										{
+											"key": "ETag",
+											"value": "W/\"1fc-x4EIPFQzoLX0HenCFPx6HNfG0lc\""
+										},
+										{
+											"key": "Vary",
+											"value": "Accept-Encoding"
+										},
+										{
+											"key": "set-cookie",
+											"value": "sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils; Path=/; HttpOnly"
+										}
+									],
+									"cookie": [],
+									"body": "{\n    \"args\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\"\n    },\n    \"headers\": {\n        \"x-forwarded-proto\": \"https\",\n        \"x-forwarded-port\": \"443\",\n        \"host\": \"postman-echo.com\",\n        \"user-agent\": \"PostmanRuntime/7.29.0\",\n        \"accept\": \"*/*\",\n        \"accept-encoding\": \"gzip, deflate, br\",\n        \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\"\n    },\n    \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}"
+								},
+								{
+									"name": "Get with params case2",
+									"originalRequest": {
+										"method": "GET",
+										"header": [],
+										"url": {
+											"raw": "https://postman-echo.com/:path?k1=v1&k3=v3",
+											"protocol": "https",
+											"host": [
+												"postman-echo",
+												"com"
+											],
+											"path": [
+												":path"
+											],
+											"query": [
+												{
+													"key": "k1",
+													"value": "v1"
+												},
+												{
+													"key": "k2",
+													"value": "v2",
+													"disabled": true
+												},
+												{
+													"key": "k3",
+													"value": "v3"
+												}
+											],
+											"variable": [
+												{
+													"key": "path",
+													"value": "get"
+												}
+											]
+										}
+									},
+									"status": "OK",
+									"code": 200,
+									"_postman_previewlanguage": "json",
+									"header": [
+										{
+											"key": "Date",
+											"value": "Mon, 16 May 2022 12:14:04 GMT"
+										},
+										{
+											"key": "Content-Type",
+											"value": "application/json; charset=utf-8"
+										},
+										{
+											"key": "Content-Length",
+											"value": "504"
+										},
+										{
+											"key": "Connection",
+											"value": "keep-alive"
+										},
+										{
+											"key": "ETag",
+											"value": "W/\"1f8-tMaKs4xmwr+3su3I8mcgR0p+ucw\""
+										},
+										{
+											"key": "Vary",
+											"value": "Accept-Encoding"
+										},
+										{
+											"key": "set-cookie",
+											"value": "sails.sid=s%3AMNuX_i0KgaP_KuuMpYB8RtCNipCGJWVw.4ETfPHxE81Omqb6Yli%2FezUU8CXyYBcN3%2Bxkx5htwh8Y; Path=/; HttpOnly"
+										}
+									],
+									"cookie": [],
+									"body": "{\n    \"args\": {\n        \"k1\": \"v1\",\n        \"k3\": \"v3\"\n    },\n    \"headers\": {\n        \"x-forwarded-proto\": \"https\",\n        \"x-forwarded-port\": \"443\",\n        \"host\": \"postman-echo.com\",\n        \"user-agent\": \"PostmanRuntime/7.29.0\",\n        \"accept\": \"*/*\",\n        \"accept-encoding\": \"gzip, deflate, br\",\n        \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils\"\n    },\n    \"url\": \"https://postman-echo.com/get?k1=v1&k3=v3\"\n}"
+								}
+							]
+						}
+					]
+				}
+			]
+		},
+		{
+			"name": "folder3",
+			"item": [
+				{
+					"name": "Post form-data",
+					"request": {
+						"method": "POST",
+						"header": [],
+						"body": {
+							"mode": "formdata",
+							"formdata": [
+								{
+									"key": "k1",
+									"value": "v1",
+									"type": "text"
+								},
+								{
+									"key": "k2",
+									"value": "v2",
+									"type": "text"
+								},
+								{
+									"key": "k3",
+									"value": "v3",
+									"type": "text",
+									"disabled": true
+								}
+							]
+						},
+						"url": {
+							"raw": "https://postman-echo.com/:path",
+							"protocol": "https",
+							"host": [
+								"postman-echo",
+								"com"
+							],
+							"path": [
+								":path"
+							],
+							"variable": [
+								{
+									"key": "path",
+									"value": "post"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Post x-www-form-urlencoded",
+					"request": {
+						"method": "POST",
+						"header": [],
+						"body": {
+							"mode": "urlencoded",
+							"urlencoded": [
+								{
+									"key": "k1",
+									"value": "v1",
+									"type": "text"
+								},
+								{
+									"key": "k2",
+									"value": "v2",
+									"type": "text"
+								},
+								{
+									"key": "k3",
+									"value": "v3",
+									"type": "text",
+									"disabled": true
+								}
+							]
+						},
+						"url": {
+							"raw": "https://postman-echo.com/:path",
+							"protocol": "https",
+							"host": [
+								"postman-echo",
+								"com"
+							],
+							"path": [
+								":path"
+							],
+							"variable": [
+								{
+									"key": "path",
+									"value": "post"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Post raw json",
+					"request": {
+						"method": "POST",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n    \"k1\": \"v1\",\n    \"k2\": \"v2\"\n}",
+							"options": {
+								"raw": {
+									"language": "json"
+								}
+							}
+						},
+						"url": {
+							"raw": "https://postman-echo.com/:path",
+							"protocol": "https",
+							"host": [
+								"postman-echo",
+								"com"
+							],
+							"path": [
+								":path"
+							],
+							"variable": [
+								{
+									"key": "path",
+									"value": "post"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Post raw text",
+					"request": {
+						"method": "POST",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": "have a nice day",
+							"options": {
+								"raw": {
+									"language": "text"
+								}
+							}
+						},
+						"url": {
+							"raw": "https://postman-echo.com/:path",
+							"protocol": "https",
+							"host": [
+								"postman-echo",
+								"com"
+							],
+							"path": [
+								":path"
+							],
+							"variable": [
+								{
+									"key": "path",
+									"value": "post"
+								}
+							]
+						}
+					},
+					"response": []
+				}
+			]
+		},
+		{
+			"name": "Get request headers",
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "User-Agent",
+						"value": "HttpRunner",
+						"type": "text"
+					},
+					{
+						"key": "User-Name",
+						"value": "bbx",
+						"type": "text",
+						"disabled": true
+					},
+					{
+						"key": "Connection",
+						"value": "close",
+						"type": "text"
+					}
+				],
+				"url": {
+					"raw": "https://postman-echo.com/:path",
+					"protocol": "https",
+					"host": [
+						"postman-echo",
+						"com"
+					],
+					"path": [
+						":path"
+					],
+					"variable": [
+						{
+							"key": "path",
+							"value": "headers"
+						}
+					]
+				}
+			},
+			"response": [
+				{
+					"name": "Get request headers case1",
+					"originalRequest": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "User-Agent",
+								"value": "HttpRunner",
+								"type": "text"
+							},
+							{
+								"key": "User-Name",
+								"value": "bbx",
+								"type": "text",
+								"disabled": true
+							},
+							{
+								"key": "Cookie",
+								"value": "Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY",
+								"type": "text"
+							}
+						],
+						"url": {
+							"raw": "https://postman-echo.com/:path",
+							"protocol": "https",
+							"host": [
+								"postman-echo",
+								"com"
+							],
+							"path": [
+								":path"
+							],
+							"variable": [
+								{
+									"key": "path",
+									"value": "headers"
+								}
+							]
+						}
+					},
+					"status": "OK",
+					"code": 200,
+					"_postman_previewlanguage": "json",
+					"header": [
+						{
+							"key": "Date",
+							"value": "Mon, 16 May 2022 12:14:25 GMT"
+						},
+						{
+							"key": "Content-Type",
+							"value": "application/json; charset=utf-8"
+						},
+						{
+							"key": "Content-Length",
+							"value": "541"
+						},
+						{
+							"key": "Connection",
+							"value": "keep-alive"
+						},
+						{
+							"key": "ETag",
+							"value": "W/\"21d-ld5UvFTaRM6lihVnvCj6mZm5Of0\""
+						},
+						{
+							"key": "Vary",
+							"value": "Accept-Encoding"
+						}
+					],
+					"cookie": [],
+					"body": "{\n    \"headers\": {\n        \"x-forwarded-proto\": \"https\",\n        \"x-forwarded-port\": \"443\",\n        \"host\": \"postman-echo.com\",\n        \"user-agent\": \"HttpRunner\",\n        \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\",\n        \"accept\": \"*/*\",\n        \"accept-encoding\": \"gzip, deflate, br\"\n    }\n}"
+				}
+			]
+		}
+	]
+}
\ No newline at end of file
diff --git a/examples/data/postman/profile.yml b/examples/data/postman/profile.yml
new file mode 100644
index 000000000..c657b5efc
--- /dev/null
+++ b/examples/data/postman/profile.yml
@@ -0,0 +1,4 @@
+headers:
+  User-Agent: "this header will be created or updated"
+cookies:
+  Cookie1: "this cookie will be created or updated"
diff --git a/examples/data/postman/profile_override.yml b/examples/data/postman/profile_override.yml
new file mode 100644
index 000000000..bc620e508
--- /dev/null
+++ b/examples/data/postman/profile_override.yml
@@ -0,0 +1,5 @@
+override: true
+headers:
+  Header1: "all original headers will be overridden"
+cookies:
+  Cookie1: "all original cookies will be overridden"
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 5f8213520..857c94441 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/denisbrodbeck/machineid v1.0.1
 	github.com/fatih/color v1.13.0
 	github.com/getsentry/sentry-go v0.13.0
+	github.com/go-openapi/spec v0.20.6
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.4.1
 	github.com/httprunner/funplugin v0.4.6
diff --git a/go.sum b/go.sum
index 3326932a7..4651a6e70 100644
--- a/go.sum
+++ b/go.sum
@@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -131,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
+github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
+github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
+github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -262,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -288,16 +301,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
 github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -348,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl
 github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
 github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -837,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -854,6 +874,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go
index 0247d1478..a4c8d6636 100644
--- a/hrp/cmd/convert.go
+++ b/hrp/cmd/convert.go
@@ -2,9 +2,7 @@ package cmd
 
 import (
 	"errors"
-	"os"
 
-	"github.com/rs/zerolog/log"
 	"github.com/spf13/cobra"
 
 	"github.com/httprunner/httprunner/v4/hrp/internal/convert"
@@ -12,37 +10,52 @@ import (
 
 var convertCmd = &cobra.Command{
 	Use:   "convert $path...",
-	Short: "convert JSON/YAML testcases to pytest/gotest scripts",
-	Args:  cobra.ExactValidArgs(1),
+	Short: "convert to JSON/YAML/gotest/pytest testcases",
+	Args:  cobra.MinimumNArgs(1),
 	PreRun: func(cmd *cobra.Command, args []string) {
 		setLogLevel(logLevel)
 	},
 	RunE: func(cmd *cobra.Command, args []string) error {
-		if !pytestFlag && !gotestFlag {
-			return errors.New("please specify convertion type")
+		var flagCount int
+		var outputType convert.OutputType
+		if toJSONFlag {
+			flagCount++
 		}
-
-		var err error
-		if gotestFlag {
-			err = convert.Convert2TestScripts("gotest", args...)
-		} else {
-			err = convert.Convert2TestScripts("pytest", args...)
+		if toYAMLFlag {
+			flagCount++
+			outputType = convert.OutputTypeYAML
+		}
+		if toGoTestFlag {
+			flagCount++
+			outputType = convert.OutputTypeGoTest
+		}
+		if toPyTestFlag {
+			flagCount++
+			outputType = convert.OutputTypePyTest
 		}
-		if err != nil {
-			log.Error().Err(err).Msg("convert test scripts failed")
-			os.Exit(1)
+		if flagCount > 1 {
+			return errors.New("please specify at most one conversion flag")
 		}
+		convert.Run(outputType, outputDir, profilePath, args)
 		return nil
 	},
 }
 
 var (
-	pytestFlag bool
-	gotestFlag bool
+	toJSONFlag   bool
+	toYAMLFlag   bool
+	toGoTestFlag bool
+	toPyTestFlag bool
+	outputDir    string
+	profilePath  string
 )
 
 func init() {
 	rootCmd.AddCommand(convertCmd)
-	convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts")
-	convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)")
+	convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
+	convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)")
+	convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)")
+	convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts")
+	convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
+	convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers (except for auto-generated headers) and cookies")
 }
diff --git a/hrp/cmd/har2case.go b/hrp/cmd/har2case.go
index eecd40ccf..7d6f19942 100644
--- a/hrp/cmd/har2case.go
+++ b/hrp/cmd/har2case.go
@@ -3,10 +3,9 @@ package cmd
 import (
 	"errors"
 
-	"github.com/rs/zerolog/log"
 	"github.com/spf13/cobra"
 
-	"github.com/httprunner/httprunner/v4/hrp/internal/har2case"
+	"github.com/httprunner/httprunner/v4/hrp/internal/convert"
 )
 
 // har2caseCmd represents the har2case command
@@ -19,54 +18,34 @@ var har2caseCmd = &cobra.Command{
 		setLogLevel(logLevel)
 	},
 	RunE: func(cmd *cobra.Command, args []string) error {
-		var outputFiles []string
-		for _, arg := range args {
-			// must choose one
-			if !genYAMLFlag && !genJSONFlag {
-				return errors.New("please select convert format type")
-			}
-			var outputPath string
-			var err error
-
-			har := har2case.NewHAR(arg)
-
-			// specify output dir
-			if outputDir != "" {
-				har.SetOutputDir(outputDir)
-			}
-
-			// specify profile
-			if profilePath != "" {
-				har.SetProfile(profilePath)
-			}
-
-			// generate json/yaml files
-			if genYAMLFlag {
-				outputPath, err = har.GenYAML()
-			} else {
-				outputPath, err = har.GenJSON() // default
-			}
-			if err != nil {
-				return err
-			}
-			outputFiles = append(outputFiles, outputPath)
+		var flagCount int
+		var har2caseOutputType convert.OutputType
+		if har2caseGenJSONFlag {
+			flagCount++
+		}
+		if har2caseGenYAMLFlag {
+			flagCount++
+			har2caseOutputType = convert.OutputTypeYAML
+		}
+		if flagCount > 1 {
+			return errors.New("please specify at most one conversion flag")
 		}
-		log.Info().Strs("output", outputFiles).Msg("convert testcase success")
+		convert.Run(har2caseOutputType, har2caseOutputDir, har2caseProfilePath, args)
 		return nil
 	},
 }
 
 var (
-	genJSONFlag bool
-	genYAMLFlag bool
-	outputDir   string
-	profilePath string
+	har2caseGenJSONFlag bool
+	har2caseGenYAMLFlag bool
+	har2caseOutputDir   string
+	har2caseProfilePath string
 )
 
 func init() {
 	rootCmd.AddCommand(har2caseCmd)
-	har2caseCmd.Flags().BoolVarP(&genJSONFlag, "to-json", "j", true, "convert to JSON format")
-	har2caseCmd.Flags().BoolVarP(&genYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
-	har2caseCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
-	har2caseCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies")
+	har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", false, "convert to JSON format (default)")
+	har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
+	har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
+	har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies")
 }
diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go
index 170c39d04..d49200847 100644
--- a/hrp/internal/builtin/utils.go
+++ b/hrp/internal/builtin/utils.go
@@ -285,7 +285,8 @@ func LoadFile(path string, structObj interface{}) (err error) {
 	if err != nil {
 		return errors.Wrap(err, "read file failed")
 	}
-
+	// remove BOM at the beginning of file
+	file = bytes.TrimLeft(file, "\xef\xbb\xbf")
 	ext := filepath.Ext(path)
 	switch ext {
 	case ".json", ".har":
@@ -383,3 +384,9 @@ func readFile(path string) ([]byte, error) {
 	}
 	return file, nil
 }
+
+func GetOutputNameWithoutExtension(path string) string {
+	base := filepath.Base(path)
+	ext := filepath.Ext(base)
+	return base[0:len(base)-len(ext)] + "_test"
+}
diff --git a/hrp/internal/convert/README.md b/hrp/internal/convert/README.md
new file mode 100644
index 000000000..da426350b
--- /dev/null
+++ b/hrp/internal/convert/README.md
@@ -0,0 +1,77 @@
+# hrp convert
+
+## 快速上手
+
+```shell
+$ hrp convert -h
+convert to JSON/YAML/gotest/pytest testcases
+
+Usage:
+  hrp convert $path... [flags]
+
+Flags:
+  -h, --help                help for convert
+  -d, --output-dir string   specify output directory, default to the same dir with har file
+  -p, --profile string      specify profile path to override headers (except for auto-generated headers) and cookies
+      --to-gotest           convert to gotest scripts (TODO)
+      --to-json             convert to JSON scripts (default true)
+      --to-pytest           convert to pytest scripts
+      --to-yaml             convert to YAML scripts
+
+Global Flags:
+      --log-json           set log to json format
+  -l, --log-level string   set log level (default "INFO")
+```
+
+`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 JSON/YAML/gotest/pytest 形态的测试用例，同时也支持测试用例各个形态之间的相互转化。
+
+该指令所有选项的详细说明如下：
+
+1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的测试用例，四个选项中最多只能指定一个，如果不指定则默认会将输入转化为 JSON 形态的测试用例
+2. `--output-dir` 后接测试用例的期望输出目录的路径，用于将转换生成的测试用例输出到对应的文件夹
+3. `--profile` 后接 profile 配置文件的路径，目前支持替换（不存在则会创建）或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息，profile 文件的后缀可以为 `json/yaml/yml`，下面给出两类 profile 配置文件的示例：
+- 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息
+```yaml
+headers:
+  Header1: "this header will be created or updated"
+cookies:
+  Cookie1: "this cookie will be created or updated"
+```
+- 根据 profile 覆盖原有的 `Headers` 和 `Cookies` 信息
+```yaml
+override: true
+headers:
+  Header1: "all original headers will be overridden"
+cookies:
+  Cookie1: "all original cookies will be overridden"
+```
+
+## 注意事项
+
+1. 输出的测试用例文件名格式为 `Postman 工程文件名称（不带拓展名）` + `_test` + `.json/.yaml/.go/.py 后缀`，如果该文件已经存在则会进行覆盖
+2. `hrp convert` 可以自动识别输入类型，因此不需要通过选项来手动制定输入类型，如遇到无法识别、不支持或转换失败的情况，则会输出错误日志并跳过，不会影响其他转换过程的正常进行
+3. 在 profile 文件中，指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是，如果不指定该字段则 profile 的默认修改模式为替换模式
+4. 输入为 JSON/YAML 测试用例时，良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异，输出的 JSON/YAML 则统一采用 Golang 引擎的风格
+
+
+## 转换流程图
+
+`hrp convert` 的转换过程流程图如下：
+![flow chart](asset/flowgram.png)
+
+## 开发进度
+
+`hrp convert` 当前的开发进度如下：
+
+| from \ to | JSON | YAML | GoTest | PyTest |
+|:---------:|:----:|:----:|:------:|:------:|
+|    HAR    |  ✅   |  ✅   |   ❌    |   ✅    |
+|  Postman  |  ✅   |  ✅   |   ❌    |   ✅    |
+|  JMeter   |  ❌   |  ❌   |   ❌    |   ❌    |
+|  Swagger  |  ❌   |  ❌   |   ❌    |   ❌    |
+|   curl    |  ❌   |  ❌   |   ❌    |   ❌    |
+| Apache ab |  ❌   |  ❌   |   ❌    |   ❌    |
+|   JSON    |  ✅   |  ✅   |   ❌    |   ✅    |
+|   YAML    |  ✅   |  ✅   |   ❌    |   ✅    |
+|  GoTest   |  ❌   |  ❌   |   ❌    |   ❌    |
+|  PyTest   |  ❌   |  ❌   |   ❌    |   ❌    |
\ No newline at end of file
diff --git a/hrp/internal/convert/asset/flowgram.png b/hrp/internal/convert/asset/flowgram.png
new file mode 100644
index 000000000..3e676ec71
Binary files /dev/null and b/hrp/internal/convert/asset/flowgram.png differ
diff --git a/hrp/internal/convert/converter.go b/hrp/internal/convert/converter.go
new file mode 100644
index 000000000..63e07690b
--- /dev/null
+++ b/hrp/internal/convert/converter.go
@@ -0,0 +1,376 @@
+package convert
+
+import (
+	_ "embed"
+	"fmt"
+	"os"
+	"path/filepath"
+	"reflect"
+
+	"github.com/go-openapi/spec"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+	"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
+)
+
+const (
+	suffixJSON   = ".json"
+	suffixYAML   = ".yaml"
+	suffixGoTest = ".go"
+	suffixPyTest = ".py"
+)
+
+type InputType int
+
+const (
+	InputTypeUnknown InputType = iota // default input type: unknown
+	InputTypeHAR
+	InputTypePostman
+	InputTypeSwagger
+	InputTypeJMeter
+	InputTypeJSON
+	InputTypeYAML
+	InputTypeGoTest
+	InputTypePyTest
+)
+
+func (inputType InputType) String() string {
+	switch inputType {
+	case InputTypeHAR:
+		return "har"
+	case InputTypePostman:
+		return "postman"
+	case InputTypeSwagger:
+		return "swagger"
+	case InputTypeJMeter:
+		return "jmeter"
+	case InputTypeJSON:
+		return "json testcase"
+	case InputTypeYAML:
+		return "yaml testcase"
+	case InputTypeGoTest:
+		return "gotest script"
+	case InputTypePyTest:
+		return "pytest script"
+	default:
+		return "unknown"
+	}
+}
+
+type OutputType int
+
+const (
+	OutputTypeJSON OutputType = iota // default output type: JSON
+	OutputTypeYAML
+	OutputTypeGoTest
+	OutputTypePyTest
+)
+
+func (outputType OutputType) String() string {
+	switch outputType {
+	case OutputTypeYAML:
+		return "yaml"
+	case OutputTypeGoTest:
+		return "gotest"
+	case OutputTypePyTest:
+		return "pytest"
+	default:
+		return "json"
+	}
+}
+
+// TCaseConverter holds the common properties of case converter
+type TCaseConverter struct {
+	InputPath   string
+	OutputDir   string
+	Profile     *Profile
+	InputType   InputType
+	OutputType  OutputType
+	CaseHAR     *CaseHar
+	CasePostman *CasePostman
+	CaseSwagger *spec.Swagger
+	TCase       *hrp.TCase
+}
+
+// Profile is used to override or update(create if not existed) original headers and cookies
+type Profile struct {
+	Override bool              `json:"override" yaml:"override"`
+	Headers  map[string]string `json:"headers" yaml:"headers"`
+	Cookies  map[string]string `json:"cookies" yaml:"cookies"`
+}
+
+func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
+	tCaseConverter = &TCaseConverter{
+		InputPath: path,
+		InputType: InputTypeUnknown,
+	}
+	extName := filepath.Ext(path)
+	if extName == "" {
+		log.Warn().Msg("extension name should be specified")
+		return
+	}
+	var err error
+	switch extName {
+	case ".har":
+		caseHAR := new(CaseHar)
+		err = builtin.LoadFile(path, caseHAR)
+		if err == nil && !reflect.ValueOf(*caseHAR).IsZero() {
+			tCaseConverter.InputType = InputTypeHAR
+			tCaseConverter.CaseHAR = caseHAR
+		}
+	case ".json":
+		tCase := new(hrp.TCase)
+		err = builtin.LoadFile(path, tCase)
+		if err == nil && !reflect.ValueOf(*tCase).IsZero() {
+			tCaseConverter.InputType = InputTypeJSON
+			tCaseConverter.TCase = tCase
+			break
+		}
+		casePostman := new(CasePostman)
+		err = builtin.LoadFile(path, casePostman)
+		// deal with postman field name conflict with swagger
+		descriptionBackup := casePostman.Info.Description
+		casePostman.Info.Description = ""
+		if err == nil && !reflect.ValueOf(*casePostman).IsZero() {
+			tCaseConverter.InputType = InputTypePostman
+			casePostman.Info.Description = descriptionBackup
+			tCaseConverter.CasePostman = casePostman
+			break
+		}
+		caseSwagger := new(spec.Swagger)
+		err = builtin.LoadFile(path, caseSwagger)
+		if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
+			tCaseConverter.InputType = InputTypeSwagger
+			tCaseConverter.CaseSwagger = caseSwagger
+		}
+	case ".yaml", ".yml":
+		tCase := new(hrp.TCase)
+		err = builtin.LoadFile(path, tCase)
+		if err == nil && !reflect.ValueOf(*tCase).IsZero() {
+			tCaseConverter.InputType = InputTypeYAML
+			tCaseConverter.TCase = tCase
+			break
+		}
+		caseSwagger := new(spec.Swagger)
+		err = builtin.LoadFile(path, caseSwagger)
+		if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
+			tCaseConverter.InputType = InputTypeSwagger
+			tCaseConverter.CaseSwagger = caseSwagger
+		}
+	case ".go": // TODO
+		tCaseConverter.InputType = InputTypeGoTest
+	case ".py": // TODO
+		tCaseConverter.InputType = InputTypePyTest
+	case ".jmx": // TODO
+		tCaseConverter.InputType = InputTypeJMeter
+	default:
+		log.Warn().
+			Str("input path", tCaseConverter.InputPath).
+			Msgf("unsupported file type: %v", extName)
+	}
+	if tCaseConverter.InputType != InputTypeUnknown {
+		log.Info().
+			Str("input path", tCaseConverter.InputPath).
+			Msgf("load case as: %s", tCaseConverter.InputType.String())
+	} else {
+		log.Error().Err(err).
+			Str("input path", tCaseConverter.InputPath).
+			Msgf("failed to load case")
+	}
+	return
+}
+
+func (c *TCaseConverter) SetProfile(path string) {
+	log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
+	profile := new(Profile)
+	err := builtin.LoadFile(path, profile)
+	if err != nil {
+		log.Warn().Str("path", path).
+			Msg("failed to load profile, ignore!")
+		return
+	}
+	c.Profile = profile
+}
+
+func (c *TCaseConverter) SetOutputDir(dir string) {
+	log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
+	c.OutputDir = dir
+}
+
+func (c *TCaseConverter) genOutputPath(suffix string) string {
+	outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
+	if c.OutputDir != "" {
+		return filepath.Join(c.OutputDir, outFileFullName)
+	} else {
+		return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
+	}
+	// TODO avoid outFileFullName conflict?
+}
+
+func (c *TCaseConverter) ToPyTest() (string, error) {
+	script := convertConfig(c.TCase.Config)
+	println(script)
+	return script, nil
+}
+
+func convertConfig(config *hrp.TConfig) string {
+	script := fmt.Sprintf("Config('%s')", config.Name)
+
+	if config.Variables != nil {
+		script += fmt.Sprintf(".variables(**{%v})", config.Variables)
+	}
+	if config.BaseURL != "" {
+		script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
+	}
+	if config.Export != nil {
+		script += fmt.Sprintf(".export(*%v)", config.Export)
+	}
+	script += fmt.Sprintf(".verify(%v)", config.Verify)
+
+	return script
+}
+
+func (c *TCaseConverter) ToGoTest() (string, error) {
+	return "", nil
+}
+
+// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
+type ICaseConverter interface {
+	Struct() *TCaseConverter
+	ToJSON() (string, error)
+	ToYAML() (string, error)
+	ToGoTest() (string, error)
+	ToPyTest() (string, error)
+}
+
+func Run(outputType OutputType, outputDir, profilePath string, args []string) {
+	// report event
+	sdk.SendEvent(sdk.EventTracking{
+		Category: "ConvertTests",
+		Action:   fmt.Sprintf("hrp convert --to-%s", outputType.String()),
+	})
+
+	// identify input and load converters
+	var iCaseConverters []ICaseConverter
+	for _, arg := range args {
+		tCaseConverter := NewTCaseConverter(arg)
+		tCaseConverter.OutputType = outputType
+		if outputDir != "" {
+			tCaseConverter.SetOutputDir(outputDir)
+		}
+		if profilePath != "" {
+			tCaseConverter.SetProfile(profilePath)
+		}
+		switch tCaseConverter.InputType {
+		case InputTypeHAR:
+			iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
+		case InputTypePostman:
+			iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
+		case InputTypeJSON:
+			iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
+		case InputTypeYAML:
+			iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
+		case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
+			log.Warn().
+				Str("input path", tCaseConverter.InputPath).
+				Msg("case type not supported yet, ignore!")
+		default:
+			log.Warn().
+				Str("input path", tCaseConverter.InputPath).
+				Msg("unknown case type, ignore!")
+		}
+	}
+
+	// start converting
+	var outputFiles []string
+	var err error
+	for _, iCaseConverter := range iCaseConverters {
+		log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
+		var outputFile string
+		switch iCaseConverter.Struct().OutputType {
+		case OutputTypeYAML:
+			outputFile, err = iCaseConverter.ToYAML()
+		case OutputTypeGoTest:
+			outputFile, err = iCaseConverter.ToGoTest()
+		case OutputTypePyTest:
+			outputFile, err = iCaseConverter.ToPyTest()
+		default:
+			outputFile, err = iCaseConverter.ToJSON()
+		}
+		if err != nil {
+			log.Error().Err(err).
+				Str("input path", iCaseConverter.Struct().InputPath).
+				Msg("error occurs during converting")
+			continue
+		}
+		outputFiles = append(outputFiles, outputFile)
+	}
+	log.Info().Strs("output files", outputFiles).Msg("conversion completed")
+}
+
+func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
+	tCase := iCaseConverter.Struct().TCase
+	if tCase == nil {
+		return nil, errors.Errorf("empty json/yaml testcase occurs")
+	}
+	profile := iCaseConverter.Struct().Profile
+	if profile == nil {
+		return tCase, nil
+	}
+	for _, step := range tCase.TestSteps {
+		// override original headers and cookies
+		if profile.Override {
+			step.Request.Headers = make(map[string]string)
+			step.Request.Cookies = make(map[string]string)
+		}
+		// update (create if not existed) original headers and cookies
+		if step.Request.Headers == nil {
+			step.Request.Headers = make(map[string]string)
+		}
+		if step.Request.Cookies == nil {
+			step.Request.Cookies = make(map[string]string)
+		}
+		for k, v := range profile.Headers {
+			step.Request.Headers[k] = v
+		}
+		for k, v := range profile.Cookies {
+			step.Request.Cookies[k] = v
+		}
+	}
+	return tCase, nil
+}
+
+func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
+	// convert to temporary json testcase
+	jsonPath, err := iCaseConverter.ToJSON()
+	inputType := iCaseConverter.Struct().InputType
+	if err != nil {
+		return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
+	}
+	defer func() {
+		if jsonPath != "" {
+			if err = os.Remove(jsonPath); err != nil {
+				log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
+			}
+		}
+	}()
+
+	// convert from temporary json testcase to pytest
+	converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
+	pyTestPath, err := converterJSON.MakePyTestScript()
+	if err != nil {
+		return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
+	}
+
+	// rename resultant pytest
+	renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
+	err = os.Rename(pyTestPath, renamedPyTestPath)
+	if err != nil {
+		log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
+		return pyTestPath, nil
+	}
+	return renamedPyTestPath, nil
+}
diff --git a/hrp/internal/har2case/har.go b/hrp/internal/convert/converter_har.go
similarity index 62%
rename from hrp/internal/har2case/har.go
rename to hrp/internal/convert/converter_har.go
index 6b98839a1..d35a90317 100644
--- a/hrp/internal/har2case/har.go
+++ b/hrp/internal/convert/converter_har.go
@@ -1,6 +1,22 @@
-package har2case
+package convert
 
-import "time"
+import (
+	"encoding/base64"
+	"fmt"
+	"net/url"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+	"github.com/httprunner/httprunner/v4/hrp/internal/json"
+)
+
+// ==================== model definition starts here ====================
 
 /*
 HTTP Archive (HAR) format
@@ -8,8 +24,8 @@ https://w3c.github.io/web-performance/specs/HAR/Overview.html
 this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
 */
 
-// Har is a container type for deserialization
-type Har struct {
+// CaseHar is a container type for deserialization
+type CaseHar struct {
 	Log Log `json:"log"`
 }
 
@@ -338,3 +354,333 @@ type TestResult struct {
 	Method    string    `json:"method"`
 	HarFile   string    `json:"harfile"`
 }
+
+// ==================== model definition ends here ====================
+
+func NewConverterHAR(converter *TCaseConverter) *ConverterHAR {
+	return &ConverterHAR{
+		converter: converter,
+	}
+}
+
+type ConverterHAR struct {
+	converter *TCaseConverter
+}
+
+func (c *ConverterHAR) Struct() *TCaseConverter {
+	return c.converter
+}
+
+func (c *ConverterHAR) ToJSON() (string, error) {
+	tCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	jsonPath := c.converter.genOutputPath(suffixJSON)
+	err = builtin.Dump2JSON(tCase, jsonPath)
+	if err != nil {
+		return "", err
+	}
+	return jsonPath, nil
+}
+
+func (c *ConverterHAR) ToYAML() (string, error) {
+	tCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	yamlPath := c.converter.genOutputPath(suffixYAML)
+	err = builtin.Dump2YAML(tCase, yamlPath)
+	if err != nil {
+		return "", err
+	}
+	return yamlPath, nil
+}
+
+func (c *ConverterHAR) ToGoTest() (string, error) {
+	//TODO implement me
+	return "", errors.New("convert from har to gotest scripts is not supported yet")
+}
+
+func (c *ConverterHAR) ToPyTest() (string, error) {
+	return convertToPyTest(c)
+}
+
+func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) {
+	teststeps, err := c.prepareTestSteps()
+	if err != nil {
+		return nil, err
+	}
+
+	tCase := &hrp.TCase{
+		Config:    c.prepareConfig(),
+		TestSteps: teststeps,
+	}
+	err = tCase.MakeCompat()
+	if err != nil {
+		return nil, err
+	}
+	return tCase, nil
+}
+
+func (c *ConverterHAR) load() (*CaseHar, error) {
+	har := c.converter.CaseHAR
+	if har == nil {
+		return nil, errors.New("empty har case occurs")
+	}
+	return har, nil
+}
+
+func (c *ConverterHAR) prepareConfig() *hrp.TConfig {
+	return hrp.NewConfig("testcase description").
+		SetVerifySSL(false)
+}
+
+func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) {
+	har, err := c.load()
+	if err != nil {
+		return nil, err
+	}
+
+	var steps []*hrp.TStep
+	for _, entry := range har.Log.Entries {
+		step, err := c.prepareTestStep(&entry)
+		if err != nil {
+			return nil, err
+		}
+		steps = append(steps, step)
+	}
+
+	return steps, nil
+}
+
+func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
+	log.Info().
+		Str("method", entry.Request.Method).
+		Str("url", entry.Request.URL).
+		Msg("convert teststep")
+
+	step := &stepFromHAR{
+		TStep: hrp.TStep{
+			Request:    &hrp.Request{},
+			Validators: make([]interface{}, 0),
+		},
+		profile: c.converter.Profile,
+	}
+	if err := step.makeRequestMethod(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestURL(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestParams(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestCookies(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestHeaders(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestBody(entry); err != nil {
+		return nil, err
+	}
+	if err := step.makeValidate(entry); err != nil {
+		return nil, err
+	}
+	return &step.TStep, nil
+}
+
+type stepFromHAR struct {
+	hrp.TStep
+	profile *Profile
+}
+
+func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
+	s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
+	return nil
+}
+
+func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
+	u, err := url.Parse(entry.Request.URL)
+	if err != nil {
+		log.Error().Err(err).Msg("make request url failed")
+		return err
+	}
+	s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
+	return nil
+}
+
+func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
+	s.Request.Params = make(map[string]interface{})
+	for _, param := range entry.Request.QueryString {
+		s.Request.Params[param.Name] = param.Value
+	}
+	return nil
+}
+
+func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
+	// use cookies from har
+	s.Request.Cookies = make(map[string]string)
+	for _, cookie := range entry.Request.Cookies {
+		s.Request.Cookies[cookie.Name] = cookie.Value
+	}
+
+	if s.profile == nil {
+		return nil
+	}
+	// override all cookies according to the profile
+	if s.profile.Override {
+		s.Request.Cookies = make(map[string]string)
+	}
+	// create or update the cookies according to the profile
+	for k, v := range s.profile.Cookies {
+		s.Request.Cookies[k] = v
+	}
+	return nil
+}
+
+func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
+	// use headers from har
+	s.Request.Headers = make(map[string]string)
+	for _, header := range entry.Request.Headers {
+		if strings.EqualFold(header.Name, "cookie") {
+			continue
+		}
+		s.Request.Headers[header.Name] = header.Value
+	}
+
+	if s.profile == nil {
+		return nil
+	}
+	// override all headers according to the profile
+	if s.profile.Override {
+		s.Request.Headers = make(map[string]string)
+	}
+	// create or update the headers according to the profile
+	for k, v := range s.profile.Headers {
+		s.Request.Headers[k] = v
+	}
+	return nil
+}
+
+func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
+	mimeType := entry.Request.PostData.MimeType
+	if mimeType == "" {
+		// GET/HEAD/DELETE without body
+		return nil
+	}
+
+	// POST/PUT with body
+	if strings.HasPrefix(mimeType, "application/json") {
+		// post json
+		var body interface{}
+		if entry.Request.PostData.Text == "" {
+			body = nil
+		} else {
+			err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
+			if err != nil {
+				log.Error().Err(err).Msg("make request body failed")
+				return err
+			}
+		}
+		s.Request.Body = body
+	} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
+		// post form
+		var paramsList []string
+		for _, param := range entry.Request.PostData.Params {
+			paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
+		}
+		s.Request.Body = strings.Join(paramsList, "&")
+	} else if strings.HasPrefix(mimeType, "text/plain") {
+		// post raw data
+		s.Request.Body = entry.Request.PostData.Text
+	} else {
+		// TODO
+		log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
+	}
+	return nil
+}
+
+func (s *stepFromHAR) makeValidate(entry *Entry) error {
+	// make validator for response status code
+	s.Validators = append(s.Validators, hrp.Validator{
+		Check:   "status_code",
+		Assert:  "equals",
+		Expect:  entry.Response.Status,
+		Message: "assert response status code",
+	})
+
+	// make validators for response headers
+	for _, header := range entry.Response.Headers {
+		// assert Content-Type
+		if strings.EqualFold(header.Name, "Content-Type") {
+			s.Validators = append(s.Validators, hrp.Validator{
+				Check:   "headers.\"Content-Type\"",
+				Assert:  "equals",
+				Expect:  header.Value,
+				Message: "assert response header Content-Type",
+			})
+		}
+	}
+
+	// make validators for response body
+	respBody := entry.Response.Content
+	if respBody.Text == "" {
+		// response body is empty
+		return nil
+	}
+	if strings.HasPrefix(respBody.MimeType, "application/json") {
+		var data []byte
+		var err error
+		// response body is json
+		if respBody.Encoding == "base64" {
+			// decode base64 text
+			data, err = base64.StdEncoding.DecodeString(respBody.Text)
+			if err != nil {
+				return errors.Wrap(err, "decode base64 error")
+			}
+		} else if respBody.Encoding == "" {
+			// no encoding
+			data = []byte(respBody.Text)
+		} else {
+			// other encoding type
+			return nil
+		}
+		// convert to json
+		var body interface{}
+		if err = json.Unmarshal(data, &body); err != nil {
+			return errors.Wrap(err, "json.Unmarshal body error")
+		}
+		jsonBody, ok := body.(map[string]interface{})
+		if !ok {
+			return fmt.Errorf("response body is not json, not matched with MimeType")
+		}
+
+		// response body is json
+		keys := make([]string, 0, len(jsonBody))
+		for k := range jsonBody {
+			keys = append(keys, k)
+		}
+		// sort map keys to keep validators in stable order
+		sort.Strings(keys)
+		for _, key := range keys {
+			value := jsonBody[key]
+			switch v := value.(type) {
+			case map[string]interface{}:
+				continue
+			case []interface{}:
+				continue
+			default:
+				s.Validators = append(s.Validators, hrp.Validator{
+					Check:   fmt.Sprintf("body.%s", key),
+					Assert:  "equals",
+					Expect:  v,
+					Message: fmt.Sprintf("assert response body %s", key),
+				})
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/hrp/internal/convert/converter_json.go b/hrp/internal/convert/converter_json.go
new file mode 100644
index 000000000..fc3801421
--- /dev/null
+++ b/hrp/internal/convert/converter_json.go
@@ -0,0 +1,86 @@
+package convert
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+	"github.com/httprunner/httprunner/v4/hrp/internal/version"
+)
+
+func NewConverterJSON(converter *TCaseConverter) *ConverterJSON {
+	return &ConverterJSON{
+		converter: converter,
+	}
+}
+
+type ConverterJSON struct {
+	converter *TCaseConverter
+}
+
+func (c *ConverterJSON) Struct() *TCaseConverter {
+	return c.converter
+}
+
+func (c *ConverterJSON) ToJSON() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	jsonPath := c.converter.genOutputPath(suffixJSON)
+	err = builtin.Dump2JSON(testCase, jsonPath)
+	if err != nil {
+		return "", err
+	}
+	return jsonPath, nil
+}
+
+func (c *ConverterJSON) ToYAML() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	yamlPath := c.converter.genOutputPath(suffixYAML)
+	err = builtin.Dump2YAML(testCase, yamlPath)
+	if err != nil {
+		return "", err
+	}
+	return yamlPath, nil
+}
+
+func (c *ConverterJSON) ToGoTest() (string, error) {
+	//TODO implement me
+	return "", errors.New("convert from json testcase to gotest scripts is not supported yet")
+}
+
+func (c *ConverterJSON) ToPyTest() (string, error) {
+	return convertToPyTest(c)
+}
+
+func (c *ConverterJSON) MakePyTestScript() (string, error) {
+	httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
+	python3, err := builtin.EnsurePython3Venv(httprunner)
+	if err != nil {
+		return "", err
+	}
+	args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath)
+	err = builtin.ExecCommand(python3, args...)
+	if err != nil {
+		return "", err
+	}
+	return c.converter.genOutputPath(suffixPyTest), nil
+}
+
+func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) {
+	tCase, err := makeTestCaseFromJSONYAML(c)
+	if err != nil {
+		return nil, err
+	}
+	err = tCase.MakeCompat()
+	if err != nil {
+		return nil, err
+	}
+	return tCase, nil
+}
diff --git a/hrp/internal/convert/converter_postman.go b/hrp/internal/convert/converter_postman.go
new file mode 100644
index 000000000..bfa9a19eb
--- /dev/null
+++ b/hrp/internal/convert/converter_postman.go
@@ -0,0 +1,488 @@
+package convert
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/url"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+	"github.com/httprunner/httprunner/v4/hrp/internal/json"
+)
+
+// ==================== model definition starts here ====================
+
+/*
+Postman Collection format reference:
+https://schema.postman.com/json/collection/v2.0.0/collection.json
+https://schema.postman.com/json/collection/v2.1.0/collection.json
+*/
+
+// CasePostman represents the postman exported file
+type CasePostman struct {
+	Info  TInfo   `json:"info"`
+	Items []TItem `json:"item"`
+}
+
+// TInfo gives information about the collection
+type TInfo struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Schema      string `json:"schema"`
+}
+
+// TItem contains the detail information of request and expected responses
+// item could be defined recursively
+type TItem struct {
+	Items     []TItem     `json:"item"`
+	Name      string      `json:"name"`
+	Request   TRequest    `json:"request"`
+	Responses []TResponse `json:"response"`
+}
+
+type TRequest struct {
+	Method      string   `json:"method"`
+	Headers     []TField `json:"header"`
+	Body        TBody    `json:"body"`
+	URL         TUrl     `json:"url"`
+	Description string   `json:"description"`
+}
+
+type TResponse struct {
+	Name            string   `json:"name"`
+	OriginalRequest TRequest `json:"originalRequest"`
+	Status          string   `json:"status"`
+	Code            int      `json:"code"`
+	Headers         []TField `json:"headers"`
+	Body            string   `json:"body"`
+}
+
+type TUrl struct {
+	Raw         string   `json:"raw"`
+	Protocol    string   `json:"protocol"`
+	Path        []string `json:"path"`
+	Description string   `json:"description"`
+	Query       []TField `json:"query"`
+	Variable    []TField `json:"variable"`
+}
+
+type TField struct {
+	Key         string `json:"key"`
+	Value       string `json:"value"`
+	Src         string `json:"src"`
+	Description string `json:"description"`
+	Type        string `json:"type"`
+	Disabled    bool   `json:"disabled"`
+	Enable      bool   `json:"enable"`
+}
+
+type TBody struct {
+	Mode       string      `json:"mode"`
+	FormData   []TField    `json:"formdata"`
+	URLEncoded []TField    `json:"urlencoded"`
+	Raw        string      `json:"raw"`
+	Disabled   bool        `json:"disabled"`
+	Options    interface{} `json:"options"`
+}
+
+// ==================== model definition ends here ====================
+
+const (
+	enumBodyRaw        = "raw"
+	enumBodyUrlEncoded = "urlencoded"
+	enumBodyFormData   = "formdata"
+	enumBodyFile       = "file"
+	enumBodyGraphQL    = "graphql"
+)
+
+const (
+	enumFieldTypeText = "text"
+	enumFieldTypeFile = "file"
+)
+
+var contentTypeMap = map[string]string{
+	"text":       "text/plain",
+	"javascript": "application/javascript",
+	"json":       "application/json",
+	"html":       "text/html",
+	"xml":        "application/xml",
+}
+
+func NewConverterPostman(converter *TCaseConverter) *ConverterPostman {
+	return &ConverterPostman{
+		converter: converter,
+	}
+}
+
+type ConverterPostman struct {
+	converter *TCaseConverter
+}
+
+func (c *ConverterPostman) Struct() *TCaseConverter {
+	return c.converter
+}
+
+func (c *ConverterPostman) ToJSON() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	jsonPath := c.converter.genOutputPath(suffixJSON)
+	err = builtin.Dump2JSON(testCase, jsonPath)
+	if err != nil {
+		return "", err
+	}
+	return jsonPath, nil
+}
+
+func (c *ConverterPostman) ToYAML() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	yamlPath := c.converter.genOutputPath(suffixYAML)
+	err = builtin.Dump2YAML(testCase, yamlPath)
+	if err != nil {
+		return "", err
+	}
+	return yamlPath, nil
+}
+
+func (c *ConverterPostman) ToGoTest() (string, error) {
+	//TODO implement me
+	return "", errors.New("convert from postman to gotest scripts is not supported yet")
+}
+
+func (c *ConverterPostman) ToPyTest() (string, error) {
+	return convertToPyTest(c)
+}
+
+func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) {
+	casePostman, err := c.load()
+	if err != nil {
+		return nil, err
+	}
+	teststeps, err := c.prepareTestSteps(casePostman)
+	if err != nil {
+		return nil, err
+	}
+	tCase := &hrp.TCase{
+		Config:    c.prepareConfig(casePostman),
+		TestSteps: teststeps,
+	}
+	err = tCase.MakeCompat()
+	if err != nil {
+		return nil, err
+	}
+	return tCase, nil
+}
+
+func (c *ConverterPostman) load() (*CasePostman, error) {
+	casePostman := c.converter.CasePostman
+	if casePostman == nil {
+		return nil, errors.New("empty postman case occurs")
+	}
+	return casePostman, nil
+}
+
+func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig {
+	return hrp.NewConfig(casePostman.Info.Name).
+		SetVerifySSL(false)
+}
+
+func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) {
+	// recursively convert collection items into a list
+	var itemList []TItem
+	for _, item := range casePostman.Items {
+		extractItemList(item, &itemList)
+	}
+
+	var steps []*hrp.TStep
+	for _, item := range itemList {
+		step, err := c.prepareTestStep(&item, steps)
+		if err != nil {
+			return nil, err
+		}
+		steps = append(steps, step)
+	}
+	return steps, nil
+}
+
+func extractItemList(item TItem, itemList *[]TItem) {
+	// current item contains no other items and request is not empty
+	if len(item.Items) == 0 {
+		if !reflect.DeepEqual(item.Request, TRequest{}) {
+			*itemList = append(*itemList, item)
+		}
+		return
+	}
+
+	// look up all items inside
+	for _, i := range item.Items {
+		// append item name
+		i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name)
+		extractItemList(i, itemList)
+	}
+}
+
+func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) {
+	log.Info().
+		Str("method", item.Request.Method).
+		Str("url", item.Request.URL.Raw).
+		Msg("convert teststep")
+
+	step := &stepFromPostman{
+		TStep: hrp.TStep{
+			Request:    &hrp.Request{},
+			Validators: make([]interface{}, 0),
+		},
+		profile: c.converter.Profile,
+	}
+	if err := step.makeRequestName(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestMethod(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestURL(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestParams(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestHeaders(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestCookies(item); err != nil {
+		return nil, err
+	}
+	if err := step.makeRequestBody(item, steps); err != nil {
+		return nil, err
+	}
+	return &step.TStep, nil
+}
+
+type stepFromPostman struct {
+	hrp.TStep
+	profile *Profile
+}
+
+// makeRequestName indicates the step name the same as item name
+func (s *stepFromPostman) makeRequestName(item *TItem) error {
+	s.Name = item.Name
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
+	s.Request.Method = hrp.HTTPMethod(item.Request.Method)
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestURL(item *TItem) error {
+	rawUrl := item.Request.URL.Raw
+	// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
+	for _, field := range item.Request.URL.Variable {
+		pathVar := ":" + field.Key
+		rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1)
+	}
+	u, err := url.Parse(rawUrl)
+	if err != nil {
+		return errors.Wrap(err, "parse URL error")
+	}
+	s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestParams(item *TItem) error {
+	s.Request.Params = make(map[string]interface{})
+	for _, field := range item.Request.URL.Query {
+		if field.Disabled {
+			continue
+		}
+		s.Request.Params[field.Key] = field.Value
+	}
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
+	// headers defined in postman collection
+	s.Request.Headers = make(map[string]string)
+	for _, field := range item.Request.Headers {
+		if field.Disabled || strings.EqualFold(field.Key, "cookie") {
+			continue
+		}
+		s.Request.Headers[field.Key] = field.Value
+	}
+
+	if s.profile == nil {
+		return nil
+	}
+	// override all headers according to the profile
+	if s.profile.Override {
+		s.Request.Headers = make(map[string]string)
+	}
+	// create or update the headers according to the profile
+	for k, v := range s.profile.Headers {
+		s.Request.Headers[k] = v
+	}
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
+	// cookies defined in postman collection
+	s.Request.Cookies = make(map[string]string)
+	for _, field := range item.Request.Headers {
+		if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
+			continue
+		}
+		s.parseRequestCookiesMap(field.Value)
+	}
+
+	if s.profile == nil {
+		return nil
+	}
+	// override all cookies according to the profile
+	if s.profile.Override {
+		s.Request.Cookies = make(map[string]string)
+	}
+	// create or update the cookies according to the profile
+	for k, v := range s.profile.Cookies {
+		s.Request.Cookies[k] = v
+	}
+	return nil
+}
+
+func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
+	for _, cookie := range strings.Split(cookies, ";") {
+		cookie = strings.TrimSpace(cookie)
+		index := strings.Index(cookie, "=")
+		if index == -1 {
+			log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
+			continue
+		}
+		s.Request.Cookies[cookie[:index]] = cookie[index+1:]
+	}
+}
+
+func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error {
+	mode := item.Request.Body.Mode
+	if mode == "" {
+		return nil
+	}
+	switch mode {
+	case enumBodyRaw:
+		return s.makeRequestBodyRaw(item)
+	case enumBodyFormData:
+		return s.makeRequestBodyFormData(item, steps)
+	case enumBodyUrlEncoded:
+		return s.makeRequestBodyUrlEncoded(item)
+	case enumBodyFile, enumBodyGraphQL:
+		return errors.Errorf("unsupported body type: %v", mode)
+	}
+	return nil
+}
+
+func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
+	defer func() {
+		if p := recover(); p != nil {
+			err = fmt.Errorf("make request body (raw) failed: %v", p)
+		}
+	}()
+
+	// extract language type, default languageType: text
+	languageType := "text"
+	iOptions := item.Request.Body.Options
+	if iOptions != nil {
+		iLanguage := iOptions.(map[string]interface{})["raw"]
+		if iLanguage != nil {
+			languageType = iLanguage.(map[string]interface{})["language"].(string)
+		}
+	}
+
+	// make request body and indicate Content-Type
+	rawBody := item.Request.Body.Raw
+	if languageType == "json" {
+		var iBody interface{}
+		err = json.Unmarshal([]byte(rawBody), &iBody)
+		if err != nil {
+			return errors.Wrap(err, "make request body (raw -> json) failed")
+		}
+		s.Request.Body = iBody
+	} else {
+		s.Request.Body = rawBody
+	}
+	s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
+	return
+}
+
+func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) {
+	defer func() {
+		if err != nil {
+			err = errors.Wrap(err, "make request body form-data failed")
+		}
+	}()
+	payload := &bytes.Buffer{}
+	writer := multipart.NewWriter(payload)
+	for _, field := range item.Request.Body.FormData {
+		if field.Disabled {
+			continue
+		}
+		// form data could be text or file
+		if field.Type == enumFieldTypeText {
+			err = writer.WriteField(field.Key, field.Value)
+			if err != nil {
+				return
+			}
+		} else if field.Type == enumFieldTypeFile {
+			err = writeFormDataFile(writer, &field)
+			if err != nil {
+				return
+			}
+		}
+	}
+	err = writer.Close()
+	s.Request.Body = payload.String()
+	s.Request.Headers["Content-Type"] = writer.FormDataContentType()
+	return
+}
+
+func writeFormDataFile(writer *multipart.Writer, field *TField) error {
+	file, err := os.Open(field.Src)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	formFile, err := writer.CreateFormFile(field.Key, filepath.Base(field.Src))
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(formFile, file)
+	return err
+}
+
+func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
+	payloadMap := make(map[string]string)
+	for _, field := range item.Request.Body.URLEncoded {
+		if field.Disabled {
+			continue
+		}
+		payloadMap[field.Key] = field.Value
+	}
+	s.Request.Body = payloadMap
+	s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"
+	return nil
+}
+
+// TODO makeValidate from example response
+func (s *stepFromPostman) makeValidate(item *TItem) error {
+	return nil
+}
diff --git a/hrp/internal/convert/converter_yaml.go b/hrp/internal/convert/converter_yaml.go
new file mode 100644
index 000000000..2ad783b16
--- /dev/null
+++ b/hrp/internal/convert/converter_yaml.go
@@ -0,0 +1,69 @@
+package convert
+
+import (
+	"github.com/pkg/errors"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+)
+
+func NewConverterYAML(converter *TCaseConverter) *ConverterYAML {
+	return &ConverterYAML{
+		converter: converter,
+	}
+}
+
+type ConverterYAML struct {
+	converter *TCaseConverter
+}
+
+func (c *ConverterYAML) Struct() *TCaseConverter {
+	return c.converter
+}
+
+func (c *ConverterYAML) ToJSON() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	jsonPath := c.converter.genOutputPath(suffixJSON)
+	err = builtin.Dump2JSON(testCase, jsonPath)
+	if err != nil {
+		return "", err
+	}
+	return jsonPath, nil
+}
+
+func (c *ConverterYAML) ToYAML() (string, error) {
+	testCase, err := c.makeTestCase()
+	if err != nil {
+		return "", err
+	}
+	yamlPath := c.converter.genOutputPath(suffixYAML)
+	err = builtin.Dump2YAML(testCase, yamlPath)
+	if err != nil {
+		return "", err
+	}
+	return yamlPath, nil
+}
+
+func (c *ConverterYAML) ToGoTest() (string, error) {
+	//TODO implement me
+	return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet")
+}
+
+func (c *ConverterYAML) ToPyTest() (string, error) {
+	return convertToPyTest(c)
+}
+
+func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) {
+	tCase, err := makeTestCaseFromJSONYAML(c)
+	if err != nil {
+		return nil, err
+	}
+	err = tCase.MakeCompat()
+	if err != nil {
+		return nil, err
+	}
+	return tCase, nil
+}
diff --git a/hrp/internal/convert/main.go b/hrp/internal/convert/main.go
deleted file mode 100644
index ea58dd6e0..000000000
--- a/hrp/internal/convert/main.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package convert
-
-import (
-	_ "embed"
-	"fmt"
-	"os"
-
-	"github.com/rs/zerolog/log"
-
-	"github.com/httprunner/httprunner/v4/hrp"
-	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
-	"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
-	"github.com/httprunner/httprunner/v4/hrp/internal/version"
-)
-
-func Convert2TestScripts(destType string, paths ...string) error {
-	// report event
-	sdk.SendEvent(sdk.EventTracking{
-		Category: "ConvertTests",
-		Action:   fmt.Sprintf("hrp convert --%s", destType),
-	})
-
-	if destType == "gotest" {
-		return convert2GoTestScripts(paths...)
-	} else {
-		// default to pytest
-		return convert2PyTestScripts(paths...)
-	}
-}
-
-func convert2PyTestScripts(paths ...string) error {
-	httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
-	python3, err := builtin.EnsurePython3Venv(httprunner)
-	if err != nil {
-		return err
-	}
-
-	args := append([]string{"-m", "httprunner", "make"}, paths...)
-	return builtin.ExecCommand(python3, args...)
-}
-
-func convert2GoTestScripts(paths ...string) error {
-	log.Warn().Msg("convert to gotest scripts is not supported yet")
-	os.Exit(1)
-
-	// TODO
-	var testCasePaths []hrp.ITestCase
-	for _, path := range paths {
-		testCasePath := hrp.TestCasePath(path)
-		testCasePaths = append(testCasePaths, &testCasePath)
-	}
-
-	testCases, err := hrp.LoadTestCases(testCasePaths...)
-	if err != nil {
-		log.Error().Err(err).Msg("failed to load testcases")
-		return err
-	}
-
-	var pytestPaths []string
-	for _, testCase := range testCases {
-		tc := testCase.ToTCase()
-		converter := CaseConverter{
-			TCase: tc,
-		}
-		pytestPath, err := converter.ToPyTest()
-		if err != nil {
-			log.Error().Err(err).
-				Str("originPath", tc.Config.Path).
-				Msg("convert to pytest failed")
-			continue
-		}
-		log.Info().
-			Str("pytestPath", pytestPath).
-			Str("originPath", tc.Config.Path).
-			Msg("convert to pytest success")
-		pytestPaths = append(pytestPaths, pytestPath)
-	}
-
-	// format pytest scripts with black
-	python3, err := builtin.EnsurePython3Venv("black")
-	if err != nil {
-		return err
-	}
-	args := append([]string{"-m", "black"}, pytestPaths...)
-	return builtin.ExecCommand(python3, args...)
-}
-
-//go:embed testcase.tmpl
-var testcaseTemplate string
-
-type CaseConverter struct {
-	*hrp.TCase
-}
-
-func (c *CaseConverter) ToPyTest() (string, error) {
-	script := convertConfig(c.TCase.Config)
-	println(script)
-	return script, nil
-}
-
-func (c *CaseConverter) ToGoTest() (string, error) {
-	return "", nil
-}
-
-func convertConfig(config *hrp.TConfig) string {
-	script := fmt.Sprintf("Config('%s')", config.Name)
-
-	if config.Variables != nil {
-		script += fmt.Sprintf(".variables(**{%v})", config.Variables)
-	}
-	if config.BaseURL != "" {
-		script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
-	}
-	if config.Export != nil {
-		script += fmt.Sprintf(".export(*%v)", config.Export)
-	}
-	script += fmt.Sprintf(".verify(%v)", config.Verify)
-
-	return script
-}
diff --git a/hrp/internal/har2case/README.md b/hrp/internal/har2case/README.md
deleted file mode 100644
index 08c0b4dc2..000000000
--- a/hrp/internal/har2case/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# har2case
-
-Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
-
-## Install
-
-## Quick Start
-
-## Examples
diff --git a/hrp/internal/har2case/core.go b/hrp/internal/har2case/core.go
deleted file mode 100644
index 258248550..000000000
--- a/hrp/internal/har2case/core.go
+++ /dev/null
@@ -1,385 +0,0 @@
-package har2case
-
-import (
-	"encoding/base64"
-	"fmt"
-	"net/url"
-	"path/filepath"
-	"sort"
-	"strings"
-
-	"github.com/pkg/errors"
-	"github.com/rs/zerolog/log"
-
-	"github.com/httprunner/httprunner/v4/hrp"
-	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
-	"github.com/httprunner/httprunner/v4/hrp/internal/json"
-	"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
-)
-
-const (
-	suffixJSON = ".json"
-	suffixYAML = ".yaml"
-)
-
-func NewHAR(path string) *har {
-	return &har{
-		path: path,
-	}
-}
-
-type har struct {
-	path       string
-	filterStr  string
-	excludeStr string
-	profile    map[string]interface{}
-	outputDir  string
-}
-
-func (h *har) SetProfile(path string) {
-	log.Info().Str("path", path).Msg("set profile")
-	h.profile = make(map[string]interface{})
-	err := builtin.LoadFile(path, h.profile)
-	if err != nil {
-		log.Warn().Str("path", path).
-			Msg("invalid profile format, ignore!")
-	}
-}
-
-func (h *har) SetOutputDir(dir string) {
-	log.Info().Str("dir", dir).Msg("set output directory")
-	h.outputDir = dir
-}
-
-func (h *har) GenJSON() (jsonPath string, err error) {
-	event := sdk.EventTracking{
-		Category: "ConvertTests",
-		Action:   "hrp har2case --to-json",
-	}
-	// report start event
-	go sdk.SendEvent(event)
-	// report running timing event
-	defer sdk.SendEvent(event.StartTiming("execution"))
-
-	tCase, err := h.makeTestCase()
-	if err != nil {
-		return "", err
-	}
-	jsonPath = h.genOutputPath(suffixJSON)
-	err = builtin.Dump2JSON(tCase, jsonPath)
-	return
-}
-
-func (h *har) GenYAML() (yamlPath string, err error) {
-	event := sdk.EventTracking{
-		Category: "ConvertTests",
-		Action:   "hrp har2case --to-yaml",
-	}
-	// report start event
-	go sdk.SendEvent(event)
-	// report running timing event
-	defer sdk.SendEvent(event.StartTiming("execution"))
-
-	tCase, err := h.makeTestCase()
-	if err != nil {
-		return "", err
-	}
-	yamlPath = h.genOutputPath(suffixYAML)
-	err = builtin.Dump2YAML(tCase, yamlPath)
-	return
-}
-
-func (h *har) makeTestCase() (*hrp.TCase, error) {
-	teststeps, err := h.prepareTestSteps()
-	if err != nil {
-		return nil, err
-	}
-
-	tCase := &hrp.TCase{
-		Config:    h.prepareConfig(),
-		TestSteps: teststeps,
-	}
-	return tCase, nil
-}
-
-func (h *har) load() (*Har, error) {
-	har := &Har{}
-	err := builtin.LoadFile(h.path, har)
-	if err != nil {
-		return nil, errors.Wrap(err, "load har failed")
-	}
-	return har, nil
-}
-
-func (h *har) prepareConfig() *hrp.TConfig {
-	return hrp.NewConfig("testcase description").
-		SetVerifySSL(false)
-}
-
-func (h *har) prepareTestSteps() ([]*hrp.TStep, error) {
-	har, err := h.load()
-	if err != nil {
-		return nil, err
-	}
-
-	var steps []*hrp.TStep
-	for _, entry := range har.Log.Entries {
-		step, err := h.prepareTestStep(&entry)
-		if err != nil {
-			return nil, err
-		}
-		steps = append(steps, step)
-	}
-
-	return steps, nil
-}
-
-func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
-	log.Info().
-		Str("method", entry.Request.Method).
-		Str("url", entry.Request.URL).
-		Msg("convert teststep")
-
-	step := &tStep{
-		TStep: hrp.TStep{
-			Request:    &hrp.Request{},
-			Validators: make([]interface{}, 0),
-		},
-		profile: h.profile,
-	}
-	if err := step.makeRequestMethod(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeRequestURL(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeRequestParams(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeRequestCookies(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeRequestHeaders(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeRequestBody(entry); err != nil {
-		return nil, err
-	}
-	if err := step.makeValidate(entry); err != nil {
-		return nil, err
-	}
-	return &step.TStep, nil
-}
-
-type tStep struct {
-	hrp.TStep
-	profile map[string]interface{}
-}
-
-func (s *tStep) makeRequestMethod(entry *Entry) error {
-	s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
-	return nil
-}
-
-func (s *tStep) makeRequestURL(entry *Entry) error {
-	u, err := url.Parse(entry.Request.URL)
-	if err != nil {
-		log.Error().Err(err).Msg("make request url failed")
-		return err
-	}
-	s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
-	return nil
-}
-
-func (s *tStep) makeRequestParams(entry *Entry) error {
-	s.Request.Params = make(map[string]interface{})
-	for _, param := range entry.Request.QueryString {
-		s.Request.Params[param.Name] = param.Value
-	}
-	return nil
-}
-
-func (s *tStep) makeRequestCookies(entry *Entry) error {
-	s.Request.Cookies = make(map[string]string)
-	cookies, ok := s.profile["cookies"]
-	if ok {
-		// use cookies from profile
-		cookies, ok := cookies.(map[string]interface{})
-		if ok {
-			for k, v := range cookies {
-				s.Request.Cookies[k] = fmt.Sprintf("%v", v)
-			}
-			return nil
-		}
-		log.Warn().Interface("cookies", cookies).
-			Msg("cookies from profile is not a map, ignore!")
-	}
-
-	// use cookies from har
-	for _, cookie := range entry.Request.Cookies {
-		s.Request.Cookies[cookie.Name] = cookie.Value
-	}
-	return nil
-}
-
-func (s *tStep) makeRequestHeaders(entry *Entry) error {
-	s.Request.Headers = make(map[string]string)
-	headers, ok := s.profile["headers"]
-	if ok {
-		// use headers from profile
-		cookies, ok := headers.(map[string]interface{})
-		if ok {
-			for k, v := range cookies {
-				s.Request.Headers[k] = fmt.Sprintf("%v", v)
-			}
-			return nil
-		}
-		log.Warn().Interface("headers", headers).
-			Msg("headers from profile is not a map, ignore!")
-	}
-
-	// use headers from har
-	for _, header := range entry.Request.Headers {
-		if strings.EqualFold(header.Name, "cookie") {
-			continue
-		}
-		s.Request.Headers[header.Name] = header.Value
-	}
-	return nil
-}
-
-func (s *tStep) makeRequestBody(entry *Entry) error {
-	mimeType := entry.Request.PostData.MimeType
-	if mimeType == "" {
-		// GET/HEAD/DELETE without body
-		return nil
-	}
-
-	// POST/PUT with body
-	if strings.HasPrefix(mimeType, "application/json") {
-		// post json
-		var body interface{}
-		if entry.Request.PostData.Text == "" {
-			body = nil
-		} else {
-			err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
-			if err != nil {
-				log.Error().Err(err).Msg("make request body failed")
-				return err
-			}
-		}
-		s.Request.Body = body
-	} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
-		// post form
-		var paramsList []string
-		for _, param := range entry.Request.PostData.Params {
-			paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
-		}
-		s.Request.Body = strings.Join(paramsList, "&")
-	} else if strings.HasPrefix(mimeType, "text/plain") {
-		// post raw data
-		s.Request.Body = entry.Request.PostData.Text
-	} else {
-		// TODO
-		log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
-	}
-	return nil
-}
-
-func (s *tStep) makeValidate(entry *Entry) error {
-	// make validator for response status code
-	s.Validators = append(s.Validators, hrp.Validator{
-		Check:   "status_code",
-		Assert:  "equals",
-		Expect:  entry.Response.Status,
-		Message: "assert response status code",
-	})
-
-	// make validators for response headers
-	for _, header := range entry.Response.Headers {
-		// assert Content-Type
-		if strings.EqualFold(header.Name, "Content-Type") {
-			s.Validators = append(s.Validators, hrp.Validator{
-				Check:   "headers.\"Content-Type\"",
-				Assert:  "equals",
-				Expect:  header.Value,
-				Message: "assert response header Content-Type",
-			})
-		}
-	}
-
-	// make validators for response body
-	respBody := entry.Response.Content
-	if respBody.Text == "" {
-		// response body is empty
-		return nil
-	}
-	if strings.HasPrefix(respBody.MimeType, "application/json") {
-		var data []byte
-		var err error
-		// response body is json
-		if respBody.Encoding == "base64" {
-			// decode base64 text
-			data, err = base64.StdEncoding.DecodeString(respBody.Text)
-			if err != nil {
-				return errors.Wrap(err, "decode base64 error")
-			}
-		} else if respBody.Encoding == "" {
-			// no encoding
-			data = []byte(respBody.Text)
-		} else {
-			// other encoding type
-			return nil
-		}
-		// convert to json
-		var body interface{}
-		if err = json.Unmarshal(data, &body); err != nil {
-			return errors.Wrap(err, "json.Unmarshal body error")
-		}
-		jsonBody, ok := body.(map[string]interface{})
-		if !ok {
-			return fmt.Errorf("response body is not json, not matched with MimeType")
-		}
-
-		// response body is json
-		keys := make([]string, 0, len(jsonBody))
-		for k := range jsonBody {
-			keys = append(keys, k)
-		}
-		// sort map keys to keep validators in stable order
-		sort.Strings(keys)
-		for _, key := range keys {
-			value := jsonBody[key]
-			switch v := value.(type) {
-			case map[string]interface{}:
-				continue
-			case []interface{}:
-				continue
-			default:
-				s.Validators = append(s.Validators, hrp.Validator{
-					Check:   fmt.Sprintf("body.%s", key),
-					Assert:  "equals",
-					Expect:  v,
-					Message: fmt.Sprintf("assert response body %s", key),
-				})
-			}
-		}
-	}
-
-	return nil
-}
-
-func (h *har) genOutputPath(suffix string) string {
-	file := getFilenameWithoutExtension(h.path) + suffix
-	if h.outputDir != "" {
-		return filepath.Join(h.outputDir, file)
-	} else {
-		return filepath.Join(filepath.Dir(h.path), file)
-	}
-}
-
-func getFilenameWithoutExtension(path string) string {
-	base := filepath.Base(path)
-	ext := filepath.Ext(base)
-	return base[0 : len(base)-len(ext)]
-}
diff --git a/httprunner/compat.py b/httprunner/compat.py
index c7352f6a4..4732783ab 100644
--- a/httprunner/compat.py
+++ b/httprunner/compat.py
@@ -1,5 +1,5 @@
 """
-This module handles compatibility issues between testcase format v2 and v3.
+This module handles compatibility issues between testcase format v2, v3 and v4.
 """
 import os
 import sys
@@ -14,9 +14,8 @@
 
 
 def convert_variables(
-    raw_variables: Union[Dict, Text], test_path: Text
+        raw_variables: Union[Dict, Text], test_path: Text
 ) -> Dict[Text, Any]:
-
     if isinstance(raw_variables, Dict):
         return raw_variables
 
@@ -33,6 +32,18 @@ def convert_variables(
         )
 
 
+def _convert_request(request: Dict) -> Dict:
+    if "body" in request:
+        content_type = ""
+        if "headers" in request and "Content-Type" in request["headers"]:
+            content_type = request["headers"]["Content-Type"]
+        if content_type.startswith("application/json"):
+            request["json"] = request.pop("body")
+        else:
+            request["data"] = request.pop("body")
+    return _sort_request_by_custom_order(request)
+
+
 def _convert_jmespath(raw: Text) -> Text:
     if not isinstance(raw, Text):
         raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}")
@@ -153,6 +164,9 @@ def _ensure_step_attachment(step: Dict) -> Dict:
         "name": step["name"],
     }
 
+    if "request" in step:
+        test_dict["request"] = _convert_request(step["request"])
+
     if "variables" in step:
         test_dict["variables"] = step["variables"]
 
@@ -181,11 +195,11 @@ def _ensure_step_attachment(step: Dict) -> Dict:
     return test_dict
 
 
-def ensure_testcase_v3_api(api_content: Dict) -> Dict:
-    logger.info("convert api in v2 to testcase format v3")
+def ensure_testcase_v4_api(api_content: Dict) -> Dict:
+    logger.info("convert api in v2/v3 to testcase format v4")
 
     teststep = {
-        "request": _sort_request_by_custom_order(api_content["request"]),
+        "request": _convert_request(api_content["request"]),
     }
     teststep.update(_ensure_step_attachment(api_content))
 
@@ -202,8 +216,8 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict:
     }
 
 
-def ensure_testcase_v3(test_content: Dict) -> Dict:
-    logger.info("ensure compatibility with testcase format v2")
+def ensure_testcase_v4(test_content: Dict) -> Dict:
+    logger.info("ensure compatibility with testcase format v2/v3")
 
     v3_content = {"config": test_content["config"], "teststeps": []}
 
@@ -221,7 +235,7 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
         teststep = {}
 
         if "request" in step:
-            teststep["request"] = _sort_request_by_custom_order(step.pop("request"))
+            pass
         elif "api" in step:
             teststep["testcase"] = step.pop("api")
         elif "testcase" in step:
diff --git a/httprunner/make.py b/httprunner/make.py
index 75a4e7832..7fe473b37 100644
--- a/httprunner/make.py
+++ b/httprunner/make.py
@@ -11,8 +11,8 @@
 from httprunner.compat import (
     convert_variables,
     ensure_path_sep,
-    ensure_testcase_v3,
-    ensure_testcase_v3_api,
+    ensure_testcase_v4,
+    ensure_testcase_v4_api,
 )
 from httprunner.loader import (
     convert_relative_project_root_dir,
@@ -332,8 +332,8 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
 
 def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
     """convert valid testcase dict to pytest file path"""
-    # ensure compatibility with testcase format v2
-    testcase = ensure_testcase_v3(testcase)
+    # ensure compatibility with testcase format v2/v3
+    testcase = ensure_testcase_v4(testcase)
 
     # validate testcase format
     load_testcase(testcase)
@@ -373,9 +373,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
         if not isinstance(test_content, Dict):
             raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
 
-        # api in v2 format, convert to v3 testcase
+        # api in v2/v3 format, convert to v4 testcase
         if "request" in test_content and "name" in test_content:
-            test_content = ensure_testcase_v3_api(test_content)
+            test_content = ensure_testcase_v4_api(test_content)
 
         test_content.setdefault("config", {})["path"] = ref_testcase_path
         ref_testcase_python_abs_path = make_testcase(test_content)
@@ -473,9 +473,9 @@ def __make(tests_path: Text):
             )
             continue
 
-        # api in v2 format, convert to v3 testcase
+        # api in v2/v3 format, convert to v4 testcase
         if "request" in test_content and "name" in test_content:
-            test_content = ensure_testcase_v3_api(test_content)
+            test_content = ensure_testcase_v4_api(test_content)
 
         if "config" not in test_content:
             logger.warning(


Test patch:
diff --git a/hrp/internal/convert/converter_gotest.go b/hrp/internal/convert/converter_gotest.go
new file mode 100644
index 000000000..863da231f
--- /dev/null
+++ b/hrp/internal/convert/converter_gotest.go
@@ -0,0 +1,60 @@
+package convert
+
+import (
+	_ "embed"
+	"os"
+
+	"github.com/rs/zerolog/log"
+
+	"github.com/httprunner/httprunner/v4/hrp"
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+)
+
+func convert2GoTestScripts(paths ...string) error {
+	log.Warn().Msg("convert to gotest scripts is not supported yet")
+	os.Exit(1)
+
+	// TODO
+	var testCasePaths []hrp.ITestCase
+	for _, path := range paths {
+		testCasePath := hrp.TestCasePath(path)
+		testCasePaths = append(testCasePaths, &testCasePath)
+	}
+
+	testCases, err := hrp.LoadTestCases(testCasePaths...)
+	if err != nil {
+		log.Error().Err(err).Msg("failed to load testcases")
+		return err
+	}
+
+	var pytestPaths []string
+	for _, testCase := range testCases {
+		tc := testCase.ToTCase()
+		converter := TCaseConverter{
+			TCase: tc,
+		}
+		pytestPath, err := converter.ToPyTest()
+		if err != nil {
+			log.Error().Err(err).
+				Str("originPath", tc.Config.Path).
+				Msg("convert to pytest failed")
+			continue
+		}
+		log.Info().
+			Str("pytestPath", pytestPath).
+			Str("originPath", tc.Config.Path).
+			Msg("convert to pytest success")
+		pytestPaths = append(pytestPaths, pytestPath)
+	}
+
+	// format pytest scripts with black
+	python3, err := builtin.EnsurePython3Venv("black")
+	if err != nil {
+		return err
+	}
+	args := append([]string{"-m", "black"}, pytestPaths...)
+	return builtin.ExecCommand(python3, args...)
+}
+
+//go:embed testcase.tmpl
+var testcaseTemplate string
diff --git a/hrp/internal/har2case/core_test.go b/hrp/internal/convert/converter_har_test.go
similarity index 78%
rename from hrp/internal/har2case/core_test.go
rename to hrp/internal/convert/converter_har_test.go
index de2ee9104..0d4daa115 100644
--- a/hrp/internal/har2case/core_test.go
+++ b/hrp/internal/convert/converter_har_test.go
@@ -1,4 +1,4 @@
-package har2case
+package convert
 
 import (
 	"testing"
@@ -9,13 +9,16 @@ import (
 )
 
 var (
-	harPath     = "../../../examples/data/har/demo.har"
-	harPath2    = "../../../examples/data/har/postman-echo.har"
-	profilePath = "../../../examples/data/har/profile.yml"
+	harPath                = "../../../examples/data/har/demo.har"
+	harPath2               = "../../../examples/data/har/postman-echo.har"
+	harProfileOverridePath = "../../../examples/data/har/profile_override.yml"
 )
 
-func TestGenJSON(t *testing.T) {
-	jsonPath, err := NewHAR(harPath).GenJSON()
+var converterHAR = NewConverterHAR(NewTCaseConverter(harPath))
+var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2))
+
+func TestHAR2JSON(t *testing.T) {
+	jsonPath, err := converterHAR.ToJSON()
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -24,8 +27,8 @@ func TestGenJSON(t *testing.T) {
 	}
 }
 
-func TestGenYAML(t *testing.T) {
-	yamlPath, err := NewHAR(harPath2).GenYAML()
+func TestHAR2YAML(t *testing.T) {
+	yamlPath, err := converterHAR2.ToYAML()
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -35,8 +38,7 @@ func TestGenYAML(t *testing.T) {
 }
 
 func TestLoadHAR(t *testing.T) {
-	har := NewHAR(harPath)
-	h, err := har.load()
+	h, err := converterHAR.load()
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -49,28 +51,28 @@ func TestLoadHAR(t *testing.T) {
 }
 
 func TestLoadHARWithProfile(t *testing.T) {
-	har := NewHAR(harPath)
-	har.SetProfile(profilePath)
-	_, err := har.load()
+	tCaseConverter := NewTCaseConverter(harPath)
+	tCaseConverter.SetProfile(harProfileOverridePath)
+	h := NewConverterHAR(tCaseConverter)
+	_, err := h.load()
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
 
 	if !assert.Equal(t,
-		map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"},
-		har.profile["headers"]) {
+		map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+		h.converter.Profile.Headers) {
 		t.Fatal()
 	}
 	if !assert.Equal(t,
-		map[string]interface{}{"UserName": "debugtalk"},
-		har.profile["cookies"]) {
+		map[string]string{"UserName": "debugtalk"},
+		h.converter.Profile.Cookies) {
 		t.Fatal()
 	}
 }
 
-func TestMakeTestCase(t *testing.T) {
-	har := NewHAR(harPath)
-	tCase, err := har.makeTestCase()
+func TestMakeTestCaseFromHAR(t *testing.T) {
+	tCase, err := converterHAR.makeTestCase()
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -135,21 +137,13 @@ func TestMakeTestCase(t *testing.T) {
 	}
 }
 
-func TestGetFilenameWithoutExtension(t *testing.T) {
-	filename := getFilenameWithoutExtension(harPath2)
-	if !assert.Equal(t, "postman-echo", filename) {
-		t.Fatal()
-	}
-}
-
 func TestMakeRequestURL(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			URL: "http://127.0.0.1:8080/api/login",
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -160,7 +154,6 @@ func TestMakeRequestURL(t *testing.T) {
 }
 
 func TestMakeRequestHeaders(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -169,7 +162,7 @@ func TestMakeRequestHeaders(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -181,9 +174,10 @@ func TestMakeRequestHeaders(t *testing.T) {
 	}
 }
 
-func TestMakeRequestHeadersWithProfile(t *testing.T) {
-	har := NewHAR("")
-	har.SetProfile(profilePath)
+func TestMakeRequestHeadersWithProfileOverride(t *testing.T) {
+	tCaseConverter := NewTCaseConverter(harPath)
+	tCaseConverter.SetProfile(harProfileOverridePath)
+	h := NewConverterHAR(tCaseConverter)
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -192,7 +186,7 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := h.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -205,7 +199,6 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
 }
 
 func TestMakeRequestCookies(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -215,7 +208,7 @@ func TestMakeRequestCookies(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -228,9 +221,10 @@ func TestMakeRequestCookies(t *testing.T) {
 	}
 }
 
-func TestMakeRequestCookiesWithProfile(t *testing.T) {
-	har := NewHAR("")
-	har.SetProfile(profilePath)
+func TestMakeRequestCookiesWithProfileOverride(t *testing.T) {
+	tCaseConverter := NewTCaseConverter(harPath)
+	tCaseConverter.SetProfile(harProfileOverridePath)
+	h := NewConverterHAR(tCaseConverter)
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -240,7 +234,7 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := h.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -253,7 +247,6 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
 }
 
 func TestMakeRequestDataParams(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -266,7 +259,7 @@ func TestMakeRequestDataParams(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -277,7 +270,6 @@ func TestMakeRequestDataParams(t *testing.T) {
 }
 
 func TestMakeRequestDataJSON(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -287,7 +279,7 @@ func TestMakeRequestDataJSON(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -298,7 +290,6 @@ func TestMakeRequestDataJSON(t *testing.T) {
 }
 
 func TestMakeRequestDataTextEmpty(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Request: Request{
 			Method: "POST",
@@ -308,7 +299,7 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
@@ -319,7 +310,6 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
 }
 
 func TestMakeValidate(t *testing.T) {
-	har := NewHAR("")
 	entry := &Entry{
 		Response: Response{
 			Status: 200,
@@ -335,7 +325,7 @@ func TestMakeValidate(t *testing.T) {
 			},
 		},
 	}
-	step, err := har.prepareTestStep(entry)
+	step, err := converterHAR.prepareTestStep(entry)
 	if !assert.NoError(t, err) {
 		t.Fatal()
 	}
diff --git a/hrp/internal/convert/converter_postman_test.go b/hrp/internal/convert/converter_postman_test.go
new file mode 100644
index 000000000..9e8ad126e
--- /dev/null
+++ b/hrp/internal/convert/converter_postman_test.go
@@ -0,0 +1,159 @@
+package convert
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	collectionPath                = "../../../examples/data/postman/postman_collection.json"
+	collectionProfileOverridePath = "../../../examples/data/postman/profile_override.yml"
+	collectionProfilePath         = "../../../examples/data/postman/profile.yml"
+)
+
+var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath))
+
+func TestPostman2JSON(t *testing.T) {
+	jsonPath, err := converterPostman.ToJSON()
+	if !assert.NoError(t, err) {
+		t.Fatal()
+	}
+	if !assert.NotEmpty(t, jsonPath) {
+		t.Fatal()
+	}
+}
+
+func TestPostman2YAML(t *testing.T) {
+	yamlPath, err := converterPostman.ToYAML()
+	if !assert.NoError(t, err) {
+		t.Fatal()
+	}
+	if !assert.NotEmpty(t, yamlPath) {
+		t.Fatal()
+	}
+}
+
+func TestLoadCollection(t *testing.T) {
+	casePostman, err := converterPostman.load()
+	if !assert.NoError(t, err) {
+		t.Fatal(err)
+	}
+	if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
+		t.Fatal()
+	}
+}
+
+func TestMakeTestCaseFromCollection(t *testing.T) {
+	tCase, err := converterPostman.makeTestCase()
+	if !assert.NoError(t, err) {
+		t.Fatal()
+	}
+	// check name
+	if !assert.Equal(t, "postman collection demo", tCase.Config.Name) {
+		t.Fatal()
+	}
+	// check method
+	if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
+		t.Fatal()
+	}
+	if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
+		t.Fatal()
+	}
+	// check url
+	if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
+		t.Fatal()
+	}
+	// check params
+	if !assert.Equal(t, "v1", tCase.TestSteps[0].Request.Params["k1"]) {
+		t.Fatal()
+	}
+	// check cookies (pass, postman collection doesn't contains cookies)
+	// check headers
+	if !assert.Contains(t, tCase.TestSteps[1].Request.Headers["Content-Type"], "multipart/form-data") {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.TestSteps[2].Request.Headers["Content-Type"]) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "application/json", tCase.TestSteps[3].Request.Headers["Content-Type"]) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "text/plain", tCase.TestSteps[4].Request.Headers["Content-Type"]) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "HttpRunner", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
+		t.Fatal()
+	}
+	// check body
+	if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
+		t.Fatal()
+	}
+	if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Body) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.TestSteps[2].Request.Body) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.TestSteps[3].Request.Body) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, "have a nice day", tCase.TestSteps[4].Request.Body) {
+		t.Fatal()
+	}
+	if !assert.Equal(t, nil, tCase.TestSteps[5].Request.Body) {
+		t.Fatal()
+	}
+}
+
+func TestMakeTestCaseWithProfileOverride(t *testing.T) {
+	tCaseConverter := NewTCaseConverter(collectionPath)
+	tCaseConverter.SetProfile(collectionProfileOverridePath)
+	c := NewConverterPostman(tCaseConverter)
+	tCase, err := c.makeTestCase()
+	if !assert.NoError(t, err) {
+		t.Fatal()
+	}
+	for _, step := range tCase.TestSteps {
+		if step.Request.Method == "GET" && !assert.Len(t, step.Request.Headers, 1) {
+			t.Fatal()
+		}
+		if step.Request.Method == "POST" && !assert.Len(t, step.Request.Headers, 2) {
+			t.Fatal()
+		}
+		if !assert.Equal(t, "all original headers will be overridden", step.Request.Headers["Header1"]) {
+			t.Fatal()
+		}
+		if !assert.Len(t, step.Request.Cookies, 1) {
+			t.Fatal()
+		}
+		if !assert.Equal(t, "all original cookies will be overridden", step.Request.Cookies["Cookie1"]) {
+			t.Fatal()
+		}
+	}
+}
+
+func TestMakeTestCaseWithProfile(t *testing.T) {
+	tCaseConverter := NewTCaseConverter(collectionPath)
+	tCaseConverter.SetProfile(collectionProfilePath)
+	c := NewConverterPostman(tCaseConverter)
+	tCase, err := c.makeTestCase()
+	if !assert.NoError(t, err) {
+		t.Fatal()
+	}
+	// create cookies Cookie1 indicated in profile
+	if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) {
+		t.Fatal()
+	}
+	// update header User-Agent indicated in profile
+	if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
+		t.Fatal()
+	}
+	// pass header Connection which is not indicated in profile
+	if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) {
+		t.Fatal()
+	}
+}
diff --git a/hrp/internal/convert/converter_pytest.go b/hrp/internal/convert/converter_pytest.go
new file mode 100644
index 000000000..8c094900e
--- /dev/null
+++ b/hrp/internal/convert/converter_pytest.go
@@ -0,0 +1,19 @@
+package convert
+
+import (
+	"fmt"
+
+	"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
+	"github.com/httprunner/httprunner/v4/hrp/internal/version"
+)
+
+func convert2PyTestScripts(paths ...string) error {
+	httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
+	python3, err := builtin.EnsurePython3Venv(httprunner)
+	if err != nil {
+		return err
+	}
+
+	args := append([]string{"-m", "httprunner", "make"}, paths...)
+	return builtin.ExecCommand(python3, args...)
+}
diff --git a/hrp/testcase.go b/hrp/testcase.go
index f8f8d2220..4182cec7d 100644
--- a/hrp/testcase.go
+++ b/hrp/testcase.go
@@ -64,7 +64,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
 		return nil, errors.New("incorrect testcase file format, expected config in file")
 	}
 
-	err = tc.makeCompat()
+	err = tc.MakeCompat()
 	if err != nil {
 		return nil, err
 	}
@@ -173,26 +173,27 @@ type TCase struct {
 	TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
 }
 
-// makeCompat converts TCase to compatible testcase
-func (tc *TCase) makeCompat() error {
-	var err error
+// MakeCompat converts TCase compatible with Golang engine style
+func (tc *TCase) MakeCompat() (err error) {
 	defer func() {
 		if p := recover(); p != nil {
-			err = fmt.Errorf("convert compat testcase error: %v", p)
+			err = fmt.Errorf("[MakeCompat] convert compat testcase error: %v", p)
 		}
 	}()
 	for _, step := range tc.TestSteps {
-		// 1. deal with request body compatible with HttpRunner
+		// 1. deal with request body compatibility
 		if step.Request != nil && step.Request.Body == nil {
 			if step.Request.Json != nil {
 				step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
 				step.Request.Body = step.Request.Json
+				step.Request.Json = nil
 			} else if step.Request.Data != nil {
 				step.Request.Body = step.Request.Data
+				step.Request.Data = nil
 			}
 		}
 
-		// 2. deal with validators compatible with HttpRunner
+		// 2. deal with validators compatibility
 		err = convertCompatValidator(step.Validators)
 		if err != nil {
 			return err
@@ -206,38 +207,46 @@ func (tc *TCase) makeCompat() error {
 
 func convertCompatValidator(Validators []interface{}) (err error) {
 	for i, iValidator := range Validators {
+		if _, ok := iValidator.(Validator); ok {
+			continue
+		}
 		validatorMap := iValidator.(map[string]interface{})
 		validator := Validator{}
-		_, checkExisted := validatorMap["check"]
-		_, assertExisted := validatorMap["assert"]
-		_, expectExisted := validatorMap["expect"]
-		// check priority: HRP > HttpRunner
+		iCheck, checkExisted := validatorMap["check"]
+		iAssert, assertExisted := validatorMap["assert"]
+		iExpect, expectExisted := validatorMap["expect"]
+		// validator check priority: Golang > Python engine style
 		if checkExisted && assertExisted && expectExisted {
-			// HRP validator format
-			validator.Check = validatorMap["check"].(string)
-			validator.Assert = validatorMap["assert"].(string)
-			validator.Expect = validatorMap["expect"]
-			if msg, existed := validatorMap["msg"]; existed {
-				validator.Message = msg.(string)
+			// Golang engine style
+			validator.Check = iCheck.(string)
+			validator.Assert = iAssert.(string)
+			validator.Expect = iExpect
+			if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
+				validator.Message = iMsg.(string)
 			}
 			validator.Check = convertCheckExpr(validator.Check)
 			Validators[i] = validator
-		} else if len(validatorMap) == 1 {
-			// HttpRunner validator format
+			continue
+		}
+		if len(validatorMap) == 1 {
+			// Python engine style
 			for assertMethod, iValidatorContent := range validatorMap {
-				checkAndExpect := iValidatorContent.([]interface{})
-				if len(checkAndExpect) != 2 {
+				validatorContent := iValidatorContent.([]interface{})
+				if len(validatorContent) > 3 {
 					return fmt.Errorf("unexpected validator format: %v", validatorMap)
 				}
-				validator.Check = checkAndExpect[0].(string)
+				validator.Check = validatorContent[0].(string)
 				validator.Assert = assertMethod
-				validator.Expect = checkAndExpect[1]
+				validator.Expect = validatorContent[1]
+				if len(validatorContent) == 3 {
+					validator.Message = validatorContent[2].(string)
+				}
 			}
 			validator.Check = convertCheckExpr(validator.Check)
 			Validators[i] = validator
-		} else {
-			return fmt.Errorf("unexpected validator format: %v", validatorMap)
+			continue
 		}
+		return fmt.Errorf("unexpected validator format: %v", validatorMap)
 	}
 	return nil
 }
diff --git a/httprunner/compat_test.py b/httprunner/compat_test.py
index 391133a18..a7877fb6c 100644
--- a/httprunner/compat_test.py
+++ b/httprunner/compat_test.py
@@ -26,6 +26,53 @@ def test_convert_variables(self):
         with self.assertRaises(exceptions.TestCaseFormatError):
             compat.convert_variables(None, "examples/data/a-b.c/1.yml")
 
+    def test_convert_request(self):
+        request_with_json_body = {
+            "method": "POST",
+            "url": "https://postman-echo.com/post",
+            "headers": {
+                "Content-Type": "application/json"
+            },
+            "body": {
+                "k1": "v1",
+                "k2": "v2"
+            }
+        }
+        self.assertEqual(
+            compat._convert_request(request_with_json_body),
+            {
+                "method": "POST",
+                "url": "https://postman-echo.com/post",
+                "headers": {
+                    "Content-Type": "application/json"
+                },
+                "json": {
+                    "k1": "v1",
+                    "k2": "v2"
+                }
+            }
+        )
+
+        request_with_text_body = {
+            "method": "POST",
+            "url": "https://postman-echo.com/post",
+            "headers": {
+                "Content-Type": "text/plain"
+            },
+            "body": "have a nice day"
+        }
+        self.assertEqual(
+            compat._convert_request(request_with_text_body),
+            {
+                "method": "POST",
+                "url": "https://postman-echo.com/post",
+                "headers": {
+                    "Content-Type": "text/plain"
+                },
+                "data": "have a nice day"
+            }
+        )
+
     def test_convert_jmespath(self):
         self.assertEqual(compat._convert_jmespath("content.abc"), "body.abc")
         self.assertEqual(compat._convert_jmespath("json.abc"), "body.abc")
@@ -85,7 +132,7 @@ def test_convert_validators(self):
             [{"eq": ["body[0].name", 201]}],
         )
 
-    def test_ensure_testcase_v3_api(self):
+    def test_ensure_testcase_v4_api(self):
         api_content = {
             "name": "get with params",
             "request": {
@@ -98,7 +145,7 @@ def test_ensure_testcase_v3_api(self):
             "validate": [{"eq": ["content.varB", 200]}, {"lt": ["json.0.varC", 0]}],
         }
         self.assertEqual(
-            compat.ensure_testcase_v3_api(api_content),
+            compat.ensure_testcase_v4_api(api_content),
             {
                 "config": {
                     "name": "get with params",
@@ -126,7 +173,7 @@ def test_ensure_testcase_v3_api(self):
             },
         )
 
-    def test_ensure_testcase_v3(self):
+    def test_ensure_testcase_v4(self):
         testcase_content = {
             "config": {"name": "xxx", "base_url": "https://httpbin.org"},
             "teststeps": [
@@ -150,7 +197,7 @@ def test_ensure_testcase_v3(self):
             ],
         }
         self.assertEqual(
-            compat.ensure_testcase_v3(testcase_content),
+            compat.ensure_testcase_v4(testcase_content),
             {
                 "config": {"name": "xxx", "base_url": "https://httpbin.org"},
                 "teststeps": [